diff --git a/src/DocDatabase.zig b/src/DocDatabase.zig index c87c185..203924f 100644 --- a/src/DocDatabase.zig +++ b/src/DocDatabase.zig @@ -14,6 +14,8 @@ pub const EntryKind = enum { method, property, constant, + enum_container, + enum_value, global_function, operator, @@ -386,6 +388,18 @@ pub fn loadFromXmlDir(arena_allocator: Allocator, tmp_allocator: Allocator, xml_ } // Process constants (with enum grouping) + const EnumGroup = struct { + entry_index: usize, + value_indices: ArrayList(usize) = .empty, + }; + var enum_groups: StringArrayHashMap(EnumGroup) = .empty; + defer { + for (enum_groups.values()) |*group| { + group.value_indices.deinit(tmp_allocator); + } + enum_groups.deinit(tmp_allocator); + } + if (class_doc.constants) |constants| { for (constants) |constant| { const sig = try buildConstantSignature(arena_allocator, constant); @@ -394,12 +408,27 @@ pub fn loadFromXmlDir(arena_allocator: Allocator, tmp_allocator: Allocator, xml_ else null; if (constant.qualifiers) |enum_name| { + var group_ptr = enum_groups.getPtr(enum_name); + if (group_ptr == null) { + const enum_key = try std.fmt.allocPrint(arena_allocator, "{s}.{s}", .{ class_doc.name, enum_name }); + try db.symbols.put(arena_allocator, enum_key, .{ + .key = enum_key, + .name = enum_name, + .parent_index = class_idx, + .kind = .enum_container, + }); + const enum_idx = db.symbols.getIndex(enum_key).?; + try member_indices.append(tmp_allocator, enum_idx); + try enum_groups.put(tmp_allocator, enum_name, .{ .entry_index = enum_idx }); + group_ptr = enum_groups.getPtr(enum_name).?; + } + // Enum-grouped constant: "ClassName.EnumName.VALUE_NAME" const dotted_key = try std.fmt.allocPrint(arena_allocator, "{s}.{s}.{s}", .{ class_doc.name, enum_name, constant.name }); const child: Entry = .{ .key = dotted_key, .name = constant.name, - .parent_index = class_idx, + .parent_index = group_ptr.?.entry_index, .kind = .enum_value, .description = desc, .signature = sig, @@ -408,7 +437,7 @@ pub fn loadFromXmlDir(arena_allocator: Allocator, tmp_allocator: Allocator, xml_ }; try db.symbols.put(arena_allocator, dotted_key, child); const child_idx = db.symbols.getIndex(dotted_key).?; - try member_indices.append(tmp_allocator, child_idx); + try group_ptr.?.value_indices.append(tmp_allocator, child_idx); } else { // Regular constant: "ClassName.CONSTANT_NAME" const dotted_key = try std.fmt.allocPrint(arena_allocator, "{s}.{s}", .{ class_doc.name, constant.name }); @@ -428,6 +457,11 @@ pub fn loadFromXmlDir(arena_allocator: Allocator, tmp_allocator: Allocator, xml_ } } + for (enum_groups.values()) |group| { + const members_slice = try arena_allocator.dupe(usize, group.value_indices.items); + db.symbols.values()[group.entry_index].members = members_slice; + } + // Process constructors if (class_doc.constructors) |constructors| { for (constructors) |ctor| { @@ -625,7 +659,11 @@ fn generateMarkdownForEntry(self: DocDatabase, allocator: Allocator, entry: Entr } if (entry.members) |member_indices| { - try self.generateMemberListings(allocator, member_indices, writer); + if (entry.kind == .enum_container) { + try self.formatEnumValueSection(member_indices, writer); + } else { + try self.generateMemberListings(allocator, member_indices, writer); + } } } @@ -655,7 +693,7 @@ fn generateMemberListings(self: DocDatabase, allocator: Allocator, member_indice .operator => try operators.append(allocator, idx), .signal => try signals.append(allocator, idx), .constant => try constants.append(allocator, idx), - .enum_value => try enums.append(allocator, idx), + .enum_container => try enums.append(allocator, idx), else => continue, } } @@ -678,6 +716,31 @@ fn formatMemberSection(self: DocDatabase, section_name: []const u8, member_indic } } +fn formatEnumValueSection(self: DocDatabase, member_indices: []usize, writer: *Writer) !void { + if (member_indices.len == 0) return; + + try writer.writeAll("\n## Values\n\n"); + for (member_indices) |idx| { + const member = self.symbols.values()[idx]; + if (member.kind != .enum_value) continue; + + try writer.print("- **{s}**", .{member.name}); + if (member.default_value) |default| { + try writer.print(" = `{s}`", .{default}); + } + + if (member.brief_description) |brief| { + try writer.print(" - {s}", .{brief}); + } else if (member.description) |desc| { + const new_line_idx = std.mem.indexOf(u8, desc, "\n"); + const first_line = if (new_line_idx) |line_idx| desc[0..line_idx] else desc; + try writer.print(" - {s}", .{first_line}); + } + + try writer.writeByte('\n'); + } +} + fn formatMemberLine(self: DocDatabase, member_idx: usize, writer: *Writer) !void { const member = self.symbols.values()[member_idx]; @@ -1160,6 +1223,53 @@ test "loadFromXmlDir groups constants with enum attribute as enum_value entries" try std.testing.expectEqual(EntryKind.enum_value, always_entry.kind); } +test "loadFromXmlDir creates fully-qualified enum container entries" { + var arena = ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const global_scope_xml = + \\ + \\ + \\ Global scope. + \\ Global constants and functions. + \\ + \\ Bottom action button. + \\ Right action button. + \\ + \\ + ; + + try tmp_dir.dir.writeFile(.{ .sub_path = "@GlobalScope.xml", .data = global_scope_xml }); + + const tmp_path = try tmp_dir.dir.realpathAlloc(std.testing.allocator, "."); + defer std.testing.allocator.free(tmp_path); + + const db = try DocDatabase.loadFromXmlDir(arena.allocator(), std.testing.allocator, tmp_path); + + const enum_entry = db.symbols.get("@GlobalScope.JoyButton").?; + try std.testing.expectEqual(EntryKind.enum_container, enum_entry.kind); + try std.testing.expectEqualStrings("JoyButton", enum_entry.name); + try std.testing.expect(enum_entry.members != null); + try std.testing.expectEqual(@as(usize, 2), enum_entry.members.?.len); + + const a_entry = db.symbols.get("@GlobalScope.JoyButton.JOY_BUTTON_A").?; + try std.testing.expectEqual(EntryKind.enum_value, a_entry.kind); + try std.testing.expectEqualStrings("0", a_entry.default_value.?); + + var allocating: Writer.Allocating = .init(std.testing.allocator); + defer allocating.deinit(); + try db.generateMarkdownForSymbol(std.testing.allocator, "@GlobalScope.JoyButton", &allocating.writer); + const written = allocating.written(); + try std.testing.expect(std.mem.indexOf(u8, written, "# @GlobalScope.JoyButton") != null); + try std.testing.expect(std.mem.indexOf(u8, written, "## Values") != null); + try std.testing.expect(std.mem.indexOf(u8, written, "JOY_BUTTON_A") != null); + try std.testing.expect(std.mem.indexOf(u8, written, "= `0`") != null); + try std.testing.expect(std.mem.indexOf(u8, written, "Bottom action button.") != null); +} + test "loadFromXmlDir registers GlobalScope functions as top-level entries" { var arena = ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); @@ -1336,12 +1446,15 @@ test "lookupSymbolExact returns SymbolNotFound for missing symbol in XML databas const tmp_path = try tmp_dir.dir.realpathAlloc(allocator, "."); defer allocator.free(tmp_path); - try tmp_dir.dir.writeFile(.{ .sub_path = "Node.xml", .data = + try tmp_dir.dir.writeFile(.{ + .sub_path = "Node.xml", + .data = \\ \\ \\ Base class. \\ Base node class. \\ + , }); var arena = std.heap.ArenaAllocator.init(allocator); diff --git a/src/cache.zig b/src/cache.zig index cf3c460..939377c 100644 --- a/src/cache.zig +++ b/src/cache.zig @@ -132,7 +132,6 @@ test "testing config has valid cache directory" { try std.testing.expect(std.mem.indexOf(u8, Config.testing.cache_dir, "gdoc") != null); } - test "ensureCacheDir creates directory if it doesn't exist" { const allocator = std.testing.allocator; @@ -181,7 +180,6 @@ test "ensureCacheDir succeeds when directory already exists" { try std.fs.deleteTreeAbsolute(test_cache); } - test "clearCache deletes cache directory" { const allocator = std.testing.allocator; const cache_dir = Config.testing.cache_dir; @@ -481,6 +479,75 @@ test "generateMarkdownCache writes all symbols to cache directory" { _ = try std.fs.openFileAbsolute(x_path, .{}); } +test "generateMarkdownCache writes and reads fully-qualified enum container" { + const allocator = std.testing.allocator; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const cache_dir = try tmp_dir.dir.realpathAlloc(allocator, "."); + defer allocator.free(cache_dir); + + var db = DocDatabase{ + .symbols = .empty, + }; + defer db.symbols.deinit(allocator); + + try db.symbols.put(allocator, "@GlobalScope", DocDatabase.Entry{ + .key = "@GlobalScope", + .name = "@GlobalScope", + .kind = .class, + }); + + var enum_members = [_]usize{ 2, 3 }; + try db.symbols.put(allocator, "@GlobalScope.JoyButton", DocDatabase.Entry{ + .key = "@GlobalScope.JoyButton", + .name = "JoyButton", + .parent_index = 0, + .kind = .enum_container, + .members = &enum_members, + }); + + try db.symbols.put(allocator, "@GlobalScope.JoyButton.JOY_BUTTON_A", DocDatabase.Entry{ + .key = "@GlobalScope.JoyButton.JOY_BUTTON_A", + .name = "JOY_BUTTON_A", + .parent_index = 1, + .kind = .enum_value, + .description = "Bottom action button.", + .default_value = "0", + }); + + try db.symbols.put(allocator, "@GlobalScope.JoyButton.JOY_BUTTON_B", DocDatabase.Entry{ + .key = "@GlobalScope.JoyButton.JOY_BUTTON_B", + .name = "JOY_BUTTON_B", + .parent_index = 1, + .kind = .enum_value, + .description = "Right action button.", + .default_value = "1", + }); + + try generateMarkdownCache(allocator, db, cache_dir); + + var output: Writer.Allocating = .init(allocator); + defer output.deinit(); + try readSymbolMarkdown(allocator, "@GlobalScope.JoyButton", cache_dir, &output.writer); + + const written = output.written(); + try std.testing.expect(std.mem.indexOf(u8, written, "# @GlobalScope.JoyButton") != null); + try std.testing.expect(std.mem.indexOf(u8, written, "## Values") != null); + try std.testing.expect(std.mem.indexOf(u8, written, "JOY_BUTTON_A") != null); + try std.testing.expect(std.mem.indexOf(u8, written, "= `0`") != null); + + var value_output: Writer.Allocating = .init(allocator); + defer value_output.deinit(); + try readSymbolMarkdown(allocator, "@GlobalScope.JoyButton.JOY_BUTTON_A", cache_dir, &value_output.writer); + + const value_written = value_output.written(); + try std.testing.expect(std.mem.indexOf(u8, value_written, "# @GlobalScope.JoyButton.JOY_BUTTON_A") != null); + try std.testing.expect(std.mem.indexOf(u8, value_written, "**Parent**: JoyButton") != null); + try std.testing.expect(std.mem.indexOf(u8, value_written, "Bottom action button.") != null); +} + test "generateMarkdownCache handles empty database" { const allocator = std.testing.allocator;