diff --git a/tests/helper.zig b/tests/helper.zig new file mode 100644 index 0000000..1f2e139 --- /dev/null +++ b/tests/helper.zig @@ -0,0 +1,102 @@ +const std = @import("std"); + +pub const Placeholder = struct { + loc: Loc, + + pub const Loc = std.zig.Token.Loc; + + pub fn placeholderSlice(self: Placeholder, source: []const u8) []const u8 { + return source[self.loc.start..self.loc.end]; + } +}; + +/// returns an array of all placeholder locations +pub fn collectPlaceholder(allocator: std.mem.Allocator, source: []const u8) ![]Placeholder { + var placeholders = std.ArrayListUnmanaged(Placeholder){}; + errdefer placeholders.deinit(allocator); + + var source_index: usize = 0; + while (std.mem.indexOfScalarPos(u8, source, source_index, '<')) |start_index| { + const end_index = std.mem.indexOfScalarPos(u8, source, start_index + 1, '>') orelse return error.Invalid; + + try placeholders.append(allocator, .{ .loc = .{ + .start = start_index, + .end = end_index, + } }); + + source_index = end_index + 1; + } + + return placeholders.toOwnedSlice(allocator); +} + +/// returns `source` without any placeholders +pub fn clearPlaceholders(allocator: std.mem.Allocator, source: []const u8) ![]const u8 { + var output = std.ArrayListUnmanaged(u8){}; + errdefer output.deinit(allocator); + + var source_index: usize = 0; + while (std.mem.indexOfScalarPos(u8, source, source_index, '<')) |start_index| { + try output.appendSlice(allocator, source[source_index..start_index]); + + source_index = std.mem.indexOfScalarPos(u8, source, start_index + 1, '>') orelse return error.Invalid; + source_index += 1; + } + try output.appendSlice(allocator, source[source_index..source.len]); + + return output.toOwnedSlice(allocator); +} + +const CollectClearPlaceholdersResult = struct { + /// placeholders relative to the `source` parameter in `collectClearPlaceholders` + placeholders: []Placeholder, + /// placeholders locations to `source` + placeholder_locations: []usize, + /// source without any placeholders + source: []const u8, + + pub fn deinit(self: @This(), allocator: std.mem.Allocator) void { + allocator.free(self.placeholders); + allocator.free(self.placeholder_locations); + allocator.free(self.source); + } +}; + +/// see `CollectClearPlaceholdersResult` +pub fn collectClearPlaceholders(allocator: std.mem.Allocator, source: []const u8) !CollectClearPlaceholdersResult { + var placeholders = std.ArrayListUnmanaged(Placeholder){}; + errdefer placeholders.deinit(allocator); + + var locations = std.ArrayListUnmanaged(usize){}; + errdefer locations.deinit(allocator); + + var new_source = std.ArrayListUnmanaged(u8){}; + errdefer new_source.deinit(allocator); + + var source_index: usize = 0; + var new_source_index: usize = 0; + while (std.mem.indexOfScalarPos(u8, source, source_index, '<')) |start_index| { + const end_index = std.mem.indexOfScalarPos(u8, source, start_index + 1, '>') orelse return error.Invalid; + + const placeholder = Placeholder{ .loc = .{ + .start = start_index + 1, + .end = end_index, + } }; + + try placeholders.append(allocator, placeholder); + + new_source_index = new_source_index + (start_index - source_index); + try locations.append(allocator, new_source_index); + try new_source.appendSlice(allocator, source[source_index..start_index]); + + source_index = end_index + 1; + } + try new_source.appendSlice(allocator, source[source_index..source.len]); + + return CollectClearPlaceholdersResult{ + .placeholders = placeholders.toOwnedSlice(allocator), + .placeholder_locations = locations.toOwnedSlice(allocator), + .source = new_source.toOwnedSlice(allocator), + }; +} + diff --git a/tests/lsp_features/inlay_hints.zig b/tests/lsp_features/inlay_hints.zig new file mode 100644 index 0000000..0c14fa8 --- /dev/null +++ b/tests/lsp_features/inlay_hints.zig @@ -0,0 +1,163 @@ +const std = @import("std"); +const zls = @import("zls"); + +const helper = @import("helper"); +const Context = @import("context").Context; + +const types = zls.types; +const requests = zls.requests; + +const allocator: std.mem.Allocator = std.testing.allocator; + +test "inlayhints - empty" { + try testInlayHints(""); +} + +test "inlayhints - function call" { + try testInlayHints( + \\fn foo(alpha: u32) void {} + \\const _ = foo(5); + ); + try testInlayHints( + \\fn foo(alpha: u32, beta: u64) void {} + \\const _ = foo(5,4); + ); + try testInlayHints( + \\fn foo(alpha: u32, beta: u64) void {} + \\const _ = foo( 3 + 2 , (3 - 2)); + ); + try testInlayHints( + \\fn foo(alpha: u32, beta: u64) void {} + \\const _ = foo( + \\ 3 + 2, + \\ (3 - 2), + \\); + ); +} + +test "inlayhints - function self parameter" { + try testInlayHints( + \\const Foo = struct { pub fn bar(self: *Foo, alpha: u32) void {} }; + \\const foo: Foo = .{}; + \\const _ = foo.bar(5); + ); + try testInlayHints( + \\const Foo = struct { pub fn bar(_: Foo, alpha: u32, beta: []const u8) void {} }; + \\const foo: Foo = .{}; + \\const _ = foo.bar(5,""); + ); +} + +test "inlayhints - builtin call" { + try testInlayHints( + \\const _ = @intCast(u32,5); + ); + try testInlayHints( + \\const _ = @memcpy(null,null,0); + ); + + try testInlayHints( + \\const _ = @sizeOf(u32); + ); + try testInlayHints( + \\const _ = @TypeOf(5); + ); +} + +fn testInlayHints(source: []const u8) !void { + const phr = try helper.collectClearPlaceholders(allocator, source); + defer phr.deinit(allocator); + + var ctx = try Context.init(); + defer ctx.deinit(); + + ctx.server.config.enable_inlay_hints = true; + ctx.server.config.inlay_hints_exclude_single_argument = false; + ctx.server.config.inlay_hints_show_builtin = true; + + const open_document = requests.OpenDocument{ + .params = .{ + .textDocument = .{ + .uri = "file:///test.zig", + // .languageId = "zig", + // .version = 420, + .text = phr.source, + }, + }, + }; + + const did_open_method = try std.json.stringifyAlloc(allocator, open_document.params, .{}); + defer allocator.free(did_open_method); + + try ctx.request("textDocument/didOpen", did_open_method, null); + + const range = types.Range{ + .start = types.Position{ .line = 0, .character = 0 }, + .end = sourceIndexPosition(phr.source, phr.source.len), + }; + + const method = try std.json.stringifyAlloc(allocator, .{ + .textDocument = .{ + .uri = "file:///test.zig", + }, + .range = range, + }, .{}); + defer allocator.free(method); + + const response_bytes = try ctx.requestAlloc("textDocument/inlayHint", method); + defer allocator.free(response_bytes); + + const InlayHint = struct { + position: types.Position, + label: []const u8, + kind: types.InlayHintKind, + }; + + const Response = struct { + jsonrpc: []const u8, + id: types.RequestId, + result: []InlayHint, + }; + + const parse_options = std.json.ParseOptions{ + .allocator = allocator, + .ignore_unknown_fields = true, + }; + var token_stream = std.json.TokenStream.init(response_bytes); + var response = try std.json.parse(Response, &token_stream, parse_options); + defer std.json.parseFree(Response, response, parse_options); + + const hints = response.result; + + try std.testing.expectEqual(phr.placeholder_locations.len, hints.len); + + outer: for (phr.placeholder_locations) |loc, i| { + const name = phr.placeholders[i].placeholderSlice(source); + + const position = sourceIndexPosition(phr.source, loc); + + for (hints) |hint| { + if (position.line != hint.position.line or position.character != hint.position.character) continue; + + try std.testing.expect(hint.label.len != 0); + const trimmedLabel = hint.label[0 .. hint.label.len - 1]; // exclude : + try std.testing.expectEqualStrings(name, trimmedLabel); + try std.testing.expectEqual(types.InlayHintKind.Parameter, hint.kind); + + continue :outer; + } + std.debug.print("Placeholder '{s}' at {}:{} (line:colon) not found!", .{ name, position.line, position.character }); + return error.PlaceholderNotFound; + } +} + +fn sourceIndexPosition(source: []const u8, index: usize) types.Position { + const line = std.mem.count(u8, source[0..index], &.{'\n'}); + const last_line_index = if (std.mem.lastIndexOfScalar(u8, source[0..index], '\n')) |idx| idx + 1 else 0; + const last_line_character = index - last_line_index; + + return types.Position{ + .line = @intCast(i64, line), + .character = @intCast(i64, last_line_character), + }; +} diff --git a/tests/tests.zig b/tests/tests.zig index 6a305fe..758c79b 100644 --- a/tests/tests.zig +++ b/tests/tests.zig @@ -7,6 +7,7 @@ test { // LSP features _ = @import("lsp_features/semantic_tokens.zig"); + _ = @import("lsp_features/inlay_hints.zig"); // TODO Language features }