Skip to content

Commit 9092d20

Browse files
benodiwalTechatrix
authored andcommitted
feat: added folding range support for imports
1 parent 7de610e commit 9092d20

File tree

2 files changed

+104
-1
lines changed

2 files changed

+104
-1
lines changed

src/features/folding_range.zig

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,38 @@ 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 recursively drills down field accesses to find the base.
27+
fn isImportOrAlias(tree: *const Ast, node: Ast.Node.Index) bool {
28+
const token = tree.nodeMainToken(node);
29+
switch (tree.nodeTag(node)) {
30+
.builtin_call_two, .builtin_call_two_comma => {
31+
// Check if this is @import("...")
32+
const builtin_name = offsets.tokenToSlice(tree, token);
33+
if (!std.mem.eql(u8, builtin_name, "@import")) return false;
34+
35+
const first_param, const second_param = tree.nodeData(node).opt_node_and_opt_node;
36+
const param_node = first_param.unwrap() orelse return false;
37+
if (second_param != .none) return false;
38+
return tree.nodeTag(param_node) == .string_literal;
39+
},
40+
.field_access => {
41+
// Field access like @import("foo").bar or std.ascii
42+
// Accept it as an import/alias pattern
43+
return true;
44+
},
45+
.identifier => {
46+
// This could be an alias like `const ascii = std.ascii`
47+
// We can't easily determine this without full symbol resolution,
48+
// so we'll be conservative and return false here.
49+
// The code_actions.zig uses full symbol lookup which is expensive.
50+
// For folding ranges, we'll only fold direct @import statements.
51+
return false;
52+
},
53+
else => return false,
54+
}
55+
}
56+
2557
const Builder = struct {
2658
allocator: std.mem.Allocator,
2759
locations: std.ArrayList(FoldingRange),
@@ -150,7 +182,40 @@ pub fn generateFoldingRanges(allocator: std.mem.Allocator, tree: *const Ast, enc
150182

151183
// TODO add folding range normal comments
152184

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

155220
for (0..tree.nodes.len) |i| {
156221
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)