Skip to content

Commit 7fe6214

Browse files
authored
Improve folding regions (#720)
1 parent ced6e97 commit 7fe6214

File tree

1 file changed

+218
-37
lines changed

1 file changed

+218
-37
lines changed

src/Server.zig

+218-37
Original file line numberDiff line numberDiff line change
@@ -2339,74 +2339,255 @@ fn codeActionHandler(server: *Server, writer: anytype, id: types.RequestId, req:
23392339
});
23402340
}
23412341

2342-
fn foldingRangeHandler(server: *Server, writer: anytype, id: types.RequestId, req: requests.FoldingRange) !void
2343-
{
2344-
const Tag = std.zig.Token.Tag;
2342+
fn foldingRangeHandler(server: *Server, writer: anytype, id: types.RequestId, req: requests.FoldingRange) !void {
2343+
const Token = std.zig.Token;
2344+
const Node = Ast.Node;
23452345
const allocator = server.arena.allocator();
23462346
const handle = server.document_store.getHandle(req.params.textDocument.uri) orelse {
23472347
log.warn("Trying to get folding ranges of non existent document {s}", .{req.params.textDocument.uri});
23482348
return try respondGeneric(writer, id, null_result_response);
23492349
};
23502350

2351+
const helper = struct {
2352+
const Inclusivity = enum { inclusive, exclusive };
2353+
/// Returns true if added.
2354+
fn maybeAddTokRange(
2355+
p_ranges: *std.ArrayList(types.FoldingRange),
2356+
tree: Ast,
2357+
start: Ast.TokenIndex,
2358+
end: Ast.TokenIndex,
2359+
end_reach: Inclusivity,
2360+
) std.mem.Allocator.Error!bool {
2361+
const can_add = !tree.tokensOnSameLine(start, end);
2362+
if (can_add) {
2363+
try addTokRange(p_ranges, tree, start, end, end_reach);
2364+
}
2365+
return can_add;
2366+
}
2367+
fn addTokRange(
2368+
p_ranges: *std.ArrayList(types.FoldingRange),
2369+
tree: Ast,
2370+
start: Ast.TokenIndex,
2371+
end: Ast.TokenIndex,
2372+
end_reach: Inclusivity,
2373+
) std.mem.Allocator.Error!void {
2374+
std.debug.assert(!std.debug.runtime_safety or !tree.tokensOnSameLine(start, end));
2375+
2376+
const start_loc = tree.tokenLocation(0, start);
2377+
const end_loc_rel = tree.tokenLocation(@intCast(Ast.ByteOffset, start_loc.line_start), end);
2378+
std.debug.assert(end_loc_rel.line != 0);
2379+
2380+
try p_ranges.append(.{
2381+
.startLine = start_loc.line,
2382+
.endLine = (start_loc.line + end_loc_rel.line) -
2383+
@boolToInt(end_reach == .exclusive),
2384+
});
2385+
}
2386+
};
2387+
23512388
// Used to store the result
23522389
var ranges = std.ArrayList(types.FoldingRange).init(allocator);
23532390

2354-
// We add opened curly braces to a stack as we go and pop one off when we find a closing brace.
2355-
// As an optimization we start with a capacity of 10 which should work well in most cases since
2356-
// people will almost never have more than 10 levels deep of nested braces.
2357-
var stack = try std.ArrayList(usize).initCapacity(allocator, 10);
2391+
const token_tags: []const Token.Tag = handle.tree.tokens.items(.tag);
2392+
const node_tags: []const Node.Tag = handle.tree.nodes.items(.tag);
23582393

2359-
// Iterate over the token tags and look for pairs of braces
2360-
for (handle.tree.tokens.items(.tag)) |tag, i| {
2361-
const token_index = @intCast(Ast.TokenIndex, i);
2362-
2363-
// If we found a `{` we add it to our stack
2364-
if (tag == Tag.l_brace) {
2365-
const line = handle.tree.tokenLocation(0, token_index).line;
2366-
try stack.append(line);
2394+
if (token_tags.len == 0) return;
2395+
if (token_tags[0] == .container_doc_comment) {
2396+
var tok: Ast.TokenIndex = 1;
2397+
while (tok < token_tags.len) : (tok += 1) {
2398+
if (token_tags[tok] != .container_doc_comment) {
2399+
break;
2400+
}
2401+
}
2402+
if (tok > 1) { // each container doc comment has its own line, so each one counts for a line
2403+
try ranges.append(.{
2404+
.startLine = 1,
2405+
.endLine = tok,
2406+
});
23672407
}
2408+
}
23682409

2369-
// If we found a close `}` we have a matching pair
2370-
if (tag == Tag.r_brace and stack.items.len > 0) {
2371-
const start_line = stack.pop();
2372-
const end_line = handle.tree.tokenLocation(0, token_index).line;
2373-
2374-
// Add brace pairs but discard those from the same line, no need to waste memory on them
2375-
if (start_line != end_line)
2376-
{
2377-
try ranges.append(.{
2378-
.startLine = start_line,
2379-
.endLine = end_line,
2380-
});
2381-
}
2410+
for (node_tags) |node_tag, i| {
2411+
const node = @intCast(Node.Index, i);
2412+
2413+
switch (node_tag) {
2414+
// only fold the expression pertaining to the if statement, and the else statement, each respectively.
2415+
// TODO: Should folding multiline condition expressions also be supported? Ditto for the other control flow structures.
2416+
.@"if", .if_simple => {
2417+
const if_full = ast.ifFull(handle.tree, node);
2418+
2419+
const start_tok_1 = handle.tree.lastToken(if_full.ast.cond_expr);
2420+
const end_tok_1 = handle.tree.lastToken(if_full.ast.then_expr);
2421+
_ = try helper.maybeAddTokRange(&ranges, handle.tree, start_tok_1, end_tok_1, .inclusive);
2422+
2423+
if (if_full.ast.else_expr == 0) continue;
2424+
2425+
const start_tok_2 = if_full.else_token;
2426+
const end_tok_2 = handle.tree.lastToken(if_full.ast.else_expr);
2427+
2428+
_ = try helper.maybeAddTokRange(&ranges, handle.tree, start_tok_2, end_tok_2, .inclusive);
2429+
},
2430+
2431+
// same as if/else
2432+
.@"for",
2433+
.for_simple,
2434+
.@"while",
2435+
.while_cont,
2436+
.while_simple,
2437+
=> {
2438+
const loop_full = ast.whileAst(handle.tree, node).?;
2439+
2440+
const start_tok_1 = handle.tree.lastToken(loop_full.ast.cond_expr);
2441+
const end_tok_1 = handle.tree.lastToken(loop_full.ast.then_expr);
2442+
_ = try helper.maybeAddTokRange(&ranges, handle.tree, start_tok_1, end_tok_1, .inclusive);
2443+
2444+
if (loop_full.ast.else_expr == 0) continue;
2445+
2446+
const start_tok_2 = loop_full.else_token;
2447+
const end_tok_2 = handle.tree.lastToken(loop_full.ast.else_expr);
2448+
_ = try helper.maybeAddTokRange(&ranges, handle.tree, start_tok_2, end_tok_2, .inclusive);
2449+
},
2450+
2451+
.global_var_decl,
2452+
.simple_var_decl,
2453+
.aligned_var_decl,
2454+
.container_field_init,
2455+
.container_field_align,
2456+
.container_field,
2457+
.fn_proto,
2458+
.fn_proto_multi,
2459+
.fn_proto_one,
2460+
.fn_proto_simple,
2461+
.fn_decl,
2462+
=> decl_node_blk: {
2463+
doc_comment_range: {
2464+
const first_tok: Ast.TokenIndex = handle.tree.firstToken(node);
2465+
if (first_tok == 0) break :doc_comment_range;
2466+
2467+
const end_doc_tok = first_tok - 1;
2468+
if (token_tags[end_doc_tok] != .doc_comment) break :doc_comment_range;
2469+
2470+
var start_doc_tok = end_doc_tok;
2471+
while (start_doc_tok != 0) {
2472+
if (token_tags[start_doc_tok - 1] != .doc_comment) break;
2473+
start_doc_tok -= 1;
2474+
}
2475+
2476+
_ = try helper.maybeAddTokRange(&ranges, handle.tree, start_doc_tok, end_doc_tok, .inclusive);
2477+
}
2478+
2479+
// Function prototype folding regions
2480+
var fn_proto_buffer: [1]Node.Index = undefined;
2481+
const fn_proto = ast.fnProto(handle.tree, node, fn_proto_buffer[0..]) orelse
2482+
break :decl_node_blk;
2483+
2484+
const list_start_tok: Ast.TokenIndex = fn_proto.lparen;
2485+
const list_end_tok: Ast.TokenIndex = handle.tree.lastToken(fn_proto.ast.proto_node);
2486+
2487+
if (handle.tree.tokensOnSameLine(list_start_tok, list_end_tok)) break :decl_node_blk;
2488+
try ranges.ensureUnusedCapacity(1 + fn_proto.ast.params.len); // best guess, doesn't include anytype params
2489+
helper.addTokRange(&ranges, handle.tree, list_start_tok, list_end_tok, .exclusive) catch |err| switch (err) {
2490+
error.OutOfMemory => unreachable,
2491+
};
2492+
2493+
var it = fn_proto.iterate(&handle.tree);
2494+
while (ast.nextFnParam(&it)) |param| {
2495+
const doc_start_tok = param.first_doc_comment orelse continue;
2496+
var doc_end_tok = doc_start_tok;
2497+
2498+
while (token_tags[doc_end_tok + 1] == .doc_comment)
2499+
doc_end_tok += 1;
2500+
2501+
_ = try helper.maybeAddTokRange(&ranges, handle.tree, doc_start_tok, doc_end_tok, .inclusive);
2502+
}
2503+
},
2504+
2505+
.@"catch",
2506+
.@"orelse",
2507+
.multiline_string_literal,
2508+
// TODO: Similar to condition expressions in control flow structures, should folding multiline grouped expressions be enabled?
2509+
// .grouped_expression,
2510+
=> {
2511+
const start_tok = handle.tree.firstToken(node);
2512+
const end_tok = handle.tree.lastToken(node);
2513+
_ = try helper.maybeAddTokRange(&ranges, handle.tree, start_tok, end_tok, .inclusive);
2514+
},
2515+
2516+
// most other trivial cases can go through here.
2517+
else => {
2518+
switch (node_tag) {
2519+
.array_init,
2520+
.array_init_one,
2521+
.array_init_dot_two,
2522+
.array_init_one_comma,
2523+
.array_init_dot_two_comma,
2524+
.array_init_dot,
2525+
.array_init_dot_comma,
2526+
.array_init_comma,
2527+
2528+
.struct_init,
2529+
.struct_init_one,
2530+
.struct_init_one_comma,
2531+
.struct_init_dot_two,
2532+
.struct_init_dot_two_comma,
2533+
.struct_init_dot,
2534+
.struct_init_dot_comma,
2535+
.struct_init_comma,
2536+
2537+
.@"switch",
2538+
.switch_comma,
2539+
=> {},
2540+
2541+
else => disallow_fold: {
2542+
if (ast.isBlock(handle.tree, node))
2543+
break :disallow_fold;
2544+
2545+
if (ast.isCall(handle.tree, node))
2546+
break :disallow_fold;
2547+
2548+
if (ast.isBuiltinCall(handle.tree, node))
2549+
break :disallow_fold;
2550+
2551+
if (ast.isContainer(handle.tree, node) and node_tag != .root)
2552+
break :disallow_fold;
2553+
2554+
continue; // no conditions met, continue iterating without adding this potential folding range
2555+
},
2556+
}
2557+
2558+
const start_tok = handle.tree.firstToken(node);
2559+
const end_tok = handle.tree.lastToken(node);
2560+
_ = try helper.maybeAddTokRange(&ranges, handle.tree, start_tok, end_tok, .exclusive);
2561+
},
23822562
}
23832563
}
23842564

23852565
// Iterate over the source code and look for code regions with #region #endregion
23862566
{
2387-
// We will reuse the stack
2388-
stack.clearRetainingCapacity();
2567+
// We add opened folding regions to a stack as we go and pop one off when we find a closing brace.
2568+
// As an optimization we start with a reasonable capacity, which should work well in most cases since
2569+
// people will almost never have nesting that deep.
2570+
var stack = try std.ArrayList(usize).initCapacity(allocator, 10);
23892571

23902572
var i: usize = 0;
23912573
var lines_count: usize = 0;
23922574
while (i < handle.tree.source.len) : (i += 1) {
23932575
const slice = handle.tree.source[i..];
2394-
2576+
23952577
if (slice[0] == '\n') {
23962578
lines_count += 1;
23972579
}
2398-
2580+
23992581
if (std.mem.startsWith(u8, slice, "//#region")) {
24002582
try stack.append(lines_count);
24012583
}
24022584

24032585
if (std.mem.startsWith(u8, slice, "//#endregion") and stack.items.len > 0) {
24042586
const start_line = stack.pop();
24052587
const end_line = lines_count;
2406-
2588+
24072589
// Add brace pairs but discard those from the same line, no need to waste memory on them
2408-
if (start_line != end_line)
2409-
{
2590+
if (start_line != end_line) {
24102591
try ranges.append(.{
24112592
.startLine = start_line,
24122593
.endLine = end_line,
@@ -2416,7 +2597,7 @@ fn foldingRangeHandler(server: *Server, writer: anytype, id: types.RequestId, re
24162597
}
24172598
}
24182599

2419-
try send(writer, allocator, types.Response {
2600+
try send(writer, allocator, types.Response{
24202601
.id = id,
24212602
.result = .{ .FoldingRange = ranges.items },
24222603
});
@@ -2558,7 +2739,7 @@ pub fn processJsonRpc(server: *Server, writer: anytype, json: []const u8) !void
25582739
.{ "textDocument/documentHighlight", requests.DocumentHighlight, documentHighlightHandler },
25592740
.{ "textDocument/codeAction", requests.CodeAction, codeActionHandler },
25602741
.{ "workspace/didChangeConfiguration", Config.DidChangeConfigurationParams, didChangeConfigurationHandler },
2561-
.{ "textDocument/foldingRange", requests.FoldingRange, foldingRangeHandler }
2742+
.{ "textDocument/foldingRange", requests.FoldingRange, foldingRangeHandler },
25622743
};
25632744

25642745
if (zig_builtin.zig_backend == .stage1) {

0 commit comments

Comments
 (0)