Skip to content

Commit c990fe8

Browse files
authored
add folding ranges for imports and aliases (#2514)
1 parent 9d89be4 commit c990fe8

File tree

2 files changed

+102
-1
lines changed

2 files changed

+102
-1
lines changed

src/features/folding_range.zig

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,37 @@ const Inclusivity = enum {
2222
exclusive_ignore_space,
2323
};
2424

25+
/// Check if a node is an @import() call or an alias/field access based on an import.
26+
/// This drills down field accesses to find the base, assuming identifiers are aliases.
27+
fn isImportOrAlias(tree: *const Ast, init_node: Ast.Node.Index) bool {
28+
var node = init_node;
29+
while (true) {
30+
switch (tree.nodeTag(node)) {
31+
.builtin_call_two, .builtin_call_two_comma => {
32+
// Check if this is @import("...")
33+
const token = tree.nodeMainToken(node);
34+
const builtin_name = offsets.tokenToSlice(tree, token);
35+
if (!std.mem.eql(u8, builtin_name, "@import")) return false;
36+
37+
const first_param, const second_param = tree.nodeData(node).opt_node_and_opt_node;
38+
const param_node = first_param.unwrap() orelse return false;
39+
if (second_param != .none) return false;
40+
return tree.nodeTag(param_node) == .string_literal;
41+
},
42+
.field_access => {
43+
// Field access like @import("foo").bar or std.ascii
44+
// Continue drilling down to check the left side
45+
node = tree.nodeData(node).node_and_token[0];
46+
},
47+
.identifier => {
48+
// Assume identifiers are aliases like `const ascii = std.ascii`
49+
return true;
50+
},
51+
else => return false,
52+
}
53+
}
54+
}
55+
2556
const Builder = struct {
2657
allocator: std.mem.Allocator,
2758
locations: std.ArrayList(FoldingRange),
@@ -150,7 +181,39 @@ pub fn generateFoldingRanges(allocator: std.mem.Allocator, tree: *const Ast, enc
150181

151182
// TODO add folding range normal comments
152183

153-
// TODO add folding range for top level `@Import()`
184+
// Folding range for top level imports
185+
{
186+
var start_import: ?Ast.Node.Index = null;
187+
var end_import: ?Ast.Node.Index = null;
188+
189+
const root_decls = tree.rootDecls();
190+
for (root_decls) |node| {
191+
const is_import = blk: {
192+
if (tree.nodeTag(node) != .simple_var_decl) break :blk false;
193+
const var_decl = tree.simpleVarDecl(node);
194+
const init_node = var_decl.ast.init_node.unwrap() orelse break :blk false;
195+
196+
break :blk isImportOrAlias(tree, init_node);
197+
};
198+
199+
if (is_import) {
200+
if (start_import == null) {
201+
start_import = node;
202+
}
203+
end_import = node;
204+
} else if (start_import != null and end_import != null) {
205+
// We found a non-import after a sequence of imports, create folding range
206+
try builder.add(null, tree.firstToken(start_import.?), ast.lastToken(tree, end_import.?), .inclusive, .inclusive);
207+
start_import = null;
208+
end_import = null;
209+
}
210+
}
211+
212+
// Handle the case where imports continue to the end of the file
213+
if (start_import != null and end_import != null and start_import.? != end_import.?) {
214+
try builder.add(null, tree.firstToken(start_import.?), ast.lastToken(tree, end_import.?), .inclusive, .inclusive);
215+
}
216+
}
154217

155218
for (0..tree.nodes.len) |i| {
156219
const node: Ast.Node.Index = @enumFromInt(i);

tests/lsp_features/folding_range.zig

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,44 @@ test "weird code" {
376376
);
377377
}
378378

379+
test "imports" {
380+
try testFoldingRange(
381+
\\const std = @import("std");
382+
\\const builtin = @import("builtin");
383+
, &.{
384+
.{ .startLine = 0, .startCharacter = 0, .endLine = 1, .endCharacter = 34 },
385+
});
386+
try testFoldingRange(
387+
\\const std = @import("std");
388+
\\const builtin = @import("builtin");
389+
\\const lsp = @import("lsp");
390+
\\const types = lsp.types;
391+
\\
392+
\\pub fn main() void {}
393+
, &.{
394+
.{ .startLine = 0, .startCharacter = 0, .endLine = 3, .endCharacter = 23 },
395+
});
396+
// Single import should not create folding range
397+
try testFoldingRange(
398+
\\const std = @import("std");
399+
\\
400+
\\pub fn main() void {}
401+
, &.{});
402+
// Imports with gap in between should create separate folding ranges
403+
try testFoldingRange(
404+
\\const std = @import("std");
405+
\\const builtin = @import("builtin");
406+
\\
407+
\\pub const foo = 5;
408+
\\
409+
\\const lsp = @import("lsp");
410+
\\const types = @import("types");
411+
, &.{
412+
.{ .startLine = 0, .startCharacter = 0, .endLine = 1, .endCharacter = 34 },
413+
.{ .startLine = 5, .startCharacter = 0, .endLine = 6, .endCharacter = 30 },
414+
});
415+
}
416+
379417
fn testFoldingRange(source: []const u8, expect: []const types.FoldingRange) !void {
380418
var ctx: Context = try .init();
381419
defer ctx.deinit();

0 commit comments

Comments
 (0)