Skip to content

Commit ffdaa45

Browse files
committed
ensure document symbols don't have empty names
1 parent 11a3baa commit ffdaa45

File tree

2 files changed

+49
-10
lines changed

2 files changed

+49
-10
lines changed

src/features/document_symbol.zig

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const analysis = @import("../analysis.zig");
1010
const tracy = @import("tracy");
1111

1212
const Symbol = struct {
13-
name: []const u8,
13+
name_token: Ast.TokenIndex,
1414
detail: ?[]const u8 = null,
1515
kind: types.SymbolKind,
1616
loc: offsets.Loc,
@@ -27,6 +27,27 @@ const Context = struct {
2727
total_symbol_count: *usize,
2828
};
2929

30+
fn tokenNameMaybeQuotes(tree: Ast, token: Ast.TokenIndex) []const u8 {
31+
const token_slice = tree.tokenSlice(token);
32+
switch (tree.tokenTag(token)) {
33+
.identifier => return token_slice,
34+
.string_literal => {
35+
const name = token_slice[1 .. token_slice.len - 1];
36+
const trimmed = std.mem.trim(u8, name, &std.ascii.whitespace);
37+
// LSP spec requires that a symbol name not be empty or consisting only of whitespace,
38+
// don't trim the quotes in that case so there's something to present.
39+
// Leading and trailing whitespace might cause ambiguity depending on how the client shows symbols
40+
// so compensate for that as well
41+
if (name.len == 0 or
42+
name.len != trimmed.len)
43+
return token_slice;
44+
45+
return name;
46+
},
47+
else => unreachable,
48+
}
49+
}
50+
3051
fn callback(ctx: *Context, tree: Ast, node: Ast.Node.Index) error{OutOfMemory}!void {
3152
std.debug.assert(node != .root);
3253

@@ -52,7 +73,7 @@ fn callback(ctx: *Context, tree: Ast, node: Ast.Node.Index) error{OutOfMemory}!v
5273
};
5374

5475
break :blk .{
55-
.name = var_decl_name,
76+
.name_token = var_decl_name_token,
5677
.detail = null,
5778
.kind = kind,
5879
.loc = offsets.nodeToLoc(tree, node),
@@ -62,10 +83,10 @@ fn callback(ctx: *Context, tree: Ast, node: Ast.Node.Index) error{OutOfMemory}!v
6283
},
6384

6485
.test_decl => blk: {
65-
const test_name_token, const test_name = ast.testDeclNameAndToken(tree, node) orelse break :blk null;
86+
const test_name_token = tree.nodeData(node).opt_token_and_node[0].unwrap() orelse break :blk null;
6687

6788
break :blk .{
68-
.name = test_name,
89+
.name_token = test_name_token,
6990
.kind = .Method, // there is no SymbolKind that represents a tests
7091
.loc = offsets.nodeToLoc(tree, node),
7192
.selection_loc = offsets.tokenToLoc(tree, test_name_token),
@@ -79,7 +100,7 @@ fn callback(ctx: *Context, tree: Ast, node: Ast.Node.Index) error{OutOfMemory}!v
79100
const name_token = fn_info.name_token orelse break :blk null;
80101

81102
break :blk .{
82-
.name = offsets.identifierTokenToNameSlice(tree, name_token),
103+
.name_token = name_token,
83104
.detail = analysis.getFunctionSignature(tree, fn_info),
84105
.kind = .Function,
85106
.loc = offsets.nodeToLoc(tree, node),
@@ -124,10 +145,9 @@ fn callback(ctx: *Context, tree: Ast, node: Ast.Node.Index) error{OutOfMemory}!v
124145
if (is_struct and container_field.ast.tuple_like) break :blk null;
125146

126147
const decl_name_token = container_field.ast.main_token;
127-
const decl_name = offsets.tokenToSlice(tree, decl_name_token);
128148

129149
break :blk .{
130-
.name = decl_name,
150+
.name_token = decl_name_token,
131151
.detail = ctx.last_var_decl_name,
132152
.kind = kind,
133153
.loc = offsets.nodeToLoc(tree, node),
@@ -185,14 +205,15 @@ fn convertSymbols(
185205
var mappings: std.ArrayList(offsets.multiple.IndexToPositionMapping) = .empty;
186206
try mappings.ensureTotalCapacityPrecise(arena, total_symbol_count * 4);
187207

188-
const result = convertSymbolsInternal(from, &symbol_buffer, &mappings);
208+
const result = convertSymbolsInternal(tree, from, &symbol_buffer, &mappings);
189209

190210
offsets.multiple.indexToPositionWithMappings(tree.source, mappings.items, encoding);
191211

192212
return result;
193213
}
194214

195215
fn convertSymbolsInternal(
216+
tree: Ast,
196217
from: []const Symbol,
197218
symbol_buffer: *std.ArrayList(types.DocumentSymbol),
198219
mappings: *std.ArrayList(offsets.multiple.IndexToPositionMapping),
@@ -204,13 +225,13 @@ fn convertSymbolsInternal(
204225

205226
for (from, to) |symbol, *out| {
206227
out.* = .{
207-
.name = symbol.name,
228+
.name = tokenNameMaybeQuotes(tree, symbol.name_token),
208229
.detail = symbol.detail,
209230
.kind = symbol.kind,
210231
// will be set later through the mapping below
211232
.range = undefined,
212233
.selectionRange = undefined,
213-
.children = convertSymbolsInternal(symbol.children.items, symbol_buffer, mappings),
234+
.children = convertSymbolsInternal(tree, symbol.children.items, symbol_buffer, mappings),
214235
};
215236
mappings.appendSliceAssumeCapacity(&.{
216237
.{ .output = &out.range.start, .source_index = symbol.loc.start },

tests/lsp_features/document_symbol.zig

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,24 @@ test "nested struct with self" {
9393
);
9494
}
9595

96+
test "decl names that are empty or contain whitespace return non-empty document symbol" {
97+
try testDocumentSymbol(
98+
\\test "" {}
99+
\\test " " {}
100+
\\test " a " {}
101+
\\const @"" = 0;
102+
\\const @" " = 0;
103+
\\const @" a " = 0;
104+
,
105+
\\Method ""
106+
\\Method " "
107+
\\Method " a "
108+
\\Constant @""
109+
\\Constant @" "
110+
\\Constant @" a "
111+
);
112+
}
113+
96114
fn testDocumentSymbol(source: []const u8, expected: []const u8) !void {
97115
var ctx: Context = try .init();
98116
defer ctx.deinit();

0 commit comments

Comments
 (0)