Skip to content

Commit 480cd03

Browse files
committed
Add workspace symbols tests
Co-Authored-By: Techatrix <[email protected]>
1 parent c678374 commit 480cd03

File tree

8 files changed

+155
-37
lines changed

8 files changed

+155
-37
lines changed

src/DocumentStore.zig

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,7 +1096,7 @@ fn invalidateBuildFileWorker(self: *DocumentStore, build_file: *BuildFile) void
10961096

10971097
pub fn loadTrigramStores(
10981098
store: *DocumentStore,
1099-
filter_paths: []const []const u8,
1099+
filter_uris: []const std.Uri,
11001100
) error{ OutOfMemory, Canceled }![]*DocumentStore.Handle {
11011101
const tracy_zone = tracy.trace(@src());
11021102
defer tracy_zone.end();
@@ -1105,17 +1105,11 @@ pub fn loadTrigramStores(
11051105
errdefer handles.deinit(store.allocator);
11061106

11071107
for (store.handles.values()) |handle| {
1108-
if (handle.uri.toFsPath(store.allocator)) |path| {
1109-
defer store.allocator.free(path);
1110-
for (filter_paths) |filter_path| {
1111-
if (std.mem.startsWith(u8, path, filter_path)) break;
1112-
} else break;
1113-
} else |err| switch (err) {
1114-
error.OutOfMemory => return error.OutOfMemory,
1115-
else => {
1116-
// The URI is either invalid or not a `file` scheme. Either way, we should include it.
1117-
},
1118-
}
1108+
var handle_uri = std.Uri.parse(handle.uri.raw) catch unreachable;
1109+
for (filter_uris) |filter_uri| {
1110+
if (!std.ascii.eqlIgnoreCase(handle_uri.scheme, filter_uri.scheme)) continue;
1111+
if (std.mem.startsWith(u8, handle_uri.path.percent_encoded, filter_uri.path.percent_encoded)) break;
1112+
} else break;
11191113
handles.appendAssumeCapacity(handle);
11201114
}
11211115

src/Server.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -865,7 +865,7 @@ fn addWorkspace(server: *Server, uri: Uri) error{OutOfMemory}!void {
865865
}
866866

867867
const file_count = server.document_store.loadDirectoryRecursive(uri) catch |err| switch (err) {
868-
error.UnsupportedScheme => return,
868+
error.UnsupportedScheme => return, // https://github.com/microsoft/language-server-protocol/issues/1264
869869
else => {
870870
log.err("failed to load files in workspace '{s}': {}", .{ uri.raw, err });
871871
return;
@@ -1569,7 +1569,7 @@ fn selectionRangeHandler(server: *Server, arena: std.mem.Allocator, request: typ
15691569
return try selection_range.generateSelectionRanges(arena, handle, request.positions, server.offset_encoding);
15701570
}
15711571

1572-
fn workspaceSymbolHandler(server: *Server, arena: std.mem.Allocator, request: types.workspace.Symbol.Params) Error!lsp.ResultType("workspace/symbol") {
1572+
fn workspaceSymbolHandler(server: *Server, arena: std.mem.Allocator, request: types.workspace.Symbol.Params) Error!?types.workspace.Symbol.Result {
15731573
return @import("features/workspace_symbols.zig").handler(server, arena, request) catch |err| switch (err) {
15741574
error.OutOfMemory => return error.OutOfMemory,
15751575
error.Canceled => return error.InternalError, // TODO

src/TrigramStore.zig

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,12 @@ pub fn init(
148148
defer context.in_function = old_in_function;
149149

150150
switch (cb_tree.nodeTag(node)) {
151+
.fn_decl => context.in_function = true,
151152
.fn_proto,
152153
.fn_proto_multi,
153154
.fn_proto_one,
154155
.fn_proto_simple,
155-
=> |tag| skip: {
156-
context.in_function = tag == .fn_decl;
157-
156+
=> skip: {
158157
const fn_token = cb_tree.nodeMainToken(node);
159158
if (cb_tree.tokenTag(fn_token + 1) != .identifier) break :skip;
160159

@@ -204,6 +203,8 @@ pub fn init(
204203
},
205204

206205
.test_decl => skip: {
206+
context.in_function = true;
207+
207208
const test_name_token = cb_tree.nodeData(node).opt_token_and_node[0].unwrap() orelse break :skip;
208209

209210
try context.store.appendDeclaration(
@@ -297,7 +298,8 @@ fn appendDeclaration(
297298
.raw => {
298299
if (name.len < 3) return;
299300
for (0..name.len - 2) |index| {
300-
const trigram = name[index..][0..3].*;
301+
var trigram = name[index..][0..3].*;
302+
for (&trigram) |*char| char.* = std.ascii.toLower(char.*);
301303
try store.appendOneTrigram(allocator, trigram);
302304
}
303305
},

src/features/document_symbol.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const Context = struct {
2727
total_symbol_count: *usize,
2828
};
2929

30-
fn tokenNameMaybeQuotes(tree: *const Ast, token: Ast.TokenIndex) []const u8 {
30+
pub fn tokenNameMaybeQuotes(tree: *const Ast, token: Ast.TokenIndex) []const u8 {
3131
const token_slice = tree.tokenSlice(token);
3232
switch (tree.tokenTag(token)) {
3333
.identifier => return token_slice,

src/features/workspace_symbols.zig

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,19 @@ const DocumentStore = @import("../DocumentStore.zig");
99
const offsets = @import("../offsets.zig");
1010
const Server = @import("../Server.zig");
1111
const TrigramStore = @import("../TrigramStore.zig");
12+
const Uri = @import("../Uri.zig");
1213

13-
pub fn handler(server: *Server, arena: std.mem.Allocator, request: types.workspace.Symbol.Params) error{ OutOfMemory, Canceled }!lsp.ResultType("workspace/symbol") {
14+
pub fn handler(server: *Server, arena: std.mem.Allocator, request: types.workspace.Symbol.Params) error{ OutOfMemory, Canceled }!?types.workspace.Symbol.Result {
1415
if (request.query.len == 0) return null;
1516

16-
var workspace_paths: std.ArrayList([]const u8) = try .initCapacity(arena, server.workspaces.items.len);
17+
var workspace_uris: std.ArrayList(std.Uri) = try .initCapacity(arena, server.workspaces.items.len);
18+
defer workspace_uris.deinit(arena);
19+
1720
for (server.workspaces.items) |workspace| {
18-
const path = workspace.uri.toFsPath(arena) catch |err| switch (err) {
19-
error.UnsupportedScheme => return null, // https://github.com/microsoft/language-server-protocol/issues/1264
20-
error.OutOfMemory => return error.OutOfMemory,
21-
};
22-
workspace_paths.appendAssumeCapacity(path);
21+
workspace_uris.appendAssumeCapacity(std.Uri.parse(workspace.uri.raw) catch unreachable);
2322
}
2423

25-
const handles = try server.document_store.loadTrigramStores(workspace_paths.items);
24+
const handles = try server.document_store.loadTrigramStores(workspace_uris.items);
2625
defer server.document_store.allocator.free(handles);
2726

2827
var symbols: std.ArrayList(types.workspace.Symbol) = .empty;
@@ -60,12 +59,8 @@ pub fn handler(server: *Server, arena: std.mem.Allocator, request: types.workspa
6059
const name_token = names[@intFromEnum(declaration)];
6160
const kind = kinds[@intFromEnum(declaration)];
6261

63-
const loc = switch (handle.tree.tokenTag(name_token)) {
64-
.identifier => offsets.identifierTokenToNameLoc(&handle.tree, name_token),
65-
.string_literal => offsets.tokenToLoc(&handle.tree, name_token),
66-
else => unreachable,
67-
};
68-
const name = offsets.locToSlice(handle.tree.source, loc);
62+
const loc = offsets.tokenToLoc(&handle.tree, name_token);
63+
const name = @import("document_symbol.zig").tokenNameMaybeQuotes(&handle.tree, name_token);
6964

7065
const start_position = offsets.advancePosition(handle.tree.source, last_position, last_index, loc.start, server.offset_encoding);
7166
const end_position = offsets.advancePosition(handle.tree.source, start_position, loc.start, loc.end, server.offset_encoding);

tests/context.zig

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,21 @@ pub const Context = struct {
8383
use_file_scheme: bool = false,
8484
source: []const u8,
8585
mode: std.zig.Ast.Mode = .zig,
86+
base_directory: []const u8 = "/",
8687
}) !zls.Uri {
88+
std.debug.assert(std.mem.startsWith(u8, options.base_directory, "/"));
89+
std.debug.assert(std.mem.endsWith(u8, options.base_directory, "/"));
90+
8791
const fmt = switch (builtin.os.tag) {
88-
.windows => "file:///c:/Untitled-{d}.{t}",
89-
else => "file:///Untitled-{d}.{t}",
92+
.windows => "file:///c:{s}Untitled-{d}.{t}",
93+
else => "file://{s}Untitled-{d}.{t}",
9094
};
9195

9296
const arena = self.arena.allocator();
9397
const path = if (options.use_file_scheme)
94-
try std.fmt.allocPrint(arena, fmt, .{ self.file_id, options.mode })
98+
try std.fmt.allocPrint(arena, fmt, .{ options.base_directory, self.file_id, options.mode })
9599
else
96-
try std.fmt.allocPrint(arena, "untitled:///Untitled-{d}.{t}", .{ self.file_id, options.mode });
100+
try std.fmt.allocPrint(arena, "untitled://{s}Untitled-{d}.{t}", .{ options.base_directory, self.file_id, options.mode });
97101
const uri: zls.Uri = try .parse(arena, path);
98102

99103
const params: types.TextDocument.DidOpenParams = .{
@@ -110,4 +114,25 @@ pub const Context = struct {
110114
self.file_id += 1;
111115
return uri;
112116
}
117+
118+
pub fn addWorkspace(self: *Context, name: []const u8, base_directory: []const u8) !void {
119+
std.debug.assert(std.mem.startsWith(u8, base_directory, "/"));
120+
std.debug.assert(std.mem.endsWith(u8, base_directory, "/"));
121+
122+
try self.server.sendNotificationSync(
123+
self.arena.allocator(),
124+
"workspace/didChangeWorkspaceFolders",
125+
.{
126+
.event = .{
127+
.added = &.{
128+
.{
129+
.uri = try std.fmt.allocPrint(self.arena.allocator(), "untitled:{s}", .{base_directory}),
130+
.name = name,
131+
},
132+
},
133+
.removed = &.{},
134+
},
135+
},
136+
);
137+
}
113138
};
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
const std = @import("std");
2+
const zls = @import("zls");
3+
4+
const Context = @import("../context.zig").Context;
5+
6+
const types = zls.lsp.types;
7+
8+
const allocator: std.mem.Allocator = std.testing.allocator;
9+
10+
test "workspace symbols" {
11+
var ctx: Context = try .init();
12+
defer ctx.deinit();
13+
14+
try ctx.addWorkspace("Animal Shelter", "/animal_shelter/");
15+
16+
_ = try ctx.addDocument(.{ .source =
17+
\\const SalamanderCrab = struct {
18+
\\ fn salamander_crab() void {}
19+
\\};
20+
, .base_directory = "/animal_shelter/" });
21+
22+
_ = try ctx.addDocument(.{ .source =
23+
\\const Dog = struct {
24+
\\ const sheltie: Dog = .{};
25+
\\ var @"Mr Crabs" = @compileError("hold up");
26+
\\};
27+
\\test "walk the dog" {
28+
\\ const dog: Dog = .sheltie;
29+
\\ _ = dog; // nah
30+
\\}
31+
, .base_directory = "/animal_shelter/" });
32+
33+
_ = try ctx.addDocument(.{ .source =
34+
\\const Lion = struct {
35+
\\ extern fn evolveToMonke() void;
36+
\\ fn roar() void {
37+
\\ var lion = "cool!";
38+
\\ const Lion2 = struct {
39+
\\ const lion_for_real = 0;
40+
\\ };
41+
\\ }
42+
\\};
43+
, .base_directory = "/animal_shelter/" });
44+
45+
_ = try ctx.addDocument(.{ .source =
46+
\\const PotatoDoctor = struct {};
47+
, .base_directory = "/farm/" });
48+
49+
try testDocumentSymbol(&ctx, "Sal",
50+
\\Constant SalamanderCrab
51+
\\Function salamander_crab
52+
);
53+
try testDocumentSymbol(&ctx, "_cr___a_b_",
54+
\\Constant SalamanderCrab
55+
\\Function salamander_crab
56+
\\Variable @"Mr Crabs"
57+
);
58+
try testDocumentSymbol(&ctx, "dog",
59+
\\Constant Dog
60+
\\Method walk the dog
61+
);
62+
try testDocumentSymbol(&ctx, "potato_d", "");
63+
// Becomes S\x00\x00 which matches nothing
64+
try testDocumentSymbol(&ctx, "S", "");
65+
try testDocumentSymbol(&ctx, "lion",
66+
\\Constant Lion
67+
\\Constant lion_for_real
68+
);
69+
try testDocumentSymbol(&ctx, "monke",
70+
\\Function evolveToMonke
71+
);
72+
}
73+
74+
fn testDocumentSymbol(ctx: *Context, query: []const u8, expected: []const u8) !void {
75+
const response = try ctx.server.sendRequestSync(
76+
ctx.arena.allocator(),
77+
"workspace/symbol",
78+
.{ .query = query },
79+
) orelse {
80+
std.debug.print("Server returned `null` as the result\n", .{});
81+
return error.InvalidResponse;
82+
};
83+
84+
var actual: std.ArrayList(u8) = .empty;
85+
defer actual.deinit(allocator);
86+
87+
for (response.workspace_symbols) |workspace_symbol| {
88+
std.debug.assert(workspace_symbol.tags == null); // unsupported for now
89+
std.debug.assert(workspace_symbol.containerName == null); // unsupported for now
90+
try actual.print(allocator, "{t} {s}\n", .{
91+
workspace_symbol.kind,
92+
workspace_symbol.name,
93+
});
94+
}
95+
96+
if (actual.items.len != 0) {
97+
_ = actual.pop(); // Final \n
98+
}
99+
100+
try zls.testing.expectEqualStrings(expected, actual.items);
101+
}

tests/tests.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ comptime {
2222
_ = @import("lsp_features/selection_range.zig");
2323
_ = @import("lsp_features/semantic_tokens.zig");
2424
_ = @import("lsp_features/signature_help.zig");
25+
_ = @import("lsp_features/workspace_symbols.zig");
2526

2627
// Language features
2728
_ = @import("language_features/cimport.zig");

0 commit comments

Comments
 (0)