Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 118 additions & 5 deletions src/DocDatabase.zig
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ pub const EntryKind = enum {
method,
property,
constant,
enum_container,

enum_value,
global_function,
operator,
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand All @@ -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 });
Expand All @@ -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| {
Expand Down Expand Up @@ -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);
}
}
}

Expand Down Expand Up @@ -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,
}
}
Expand All @@ -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];

Expand Down Expand Up @@ -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 =
\\<?xml version="1.0" encoding="UTF-8" ?>
\\<class name="@GlobalScope">
\\ <brief_description>Global scope.</brief_description>
\\ <description>Global constants and functions.</description>
\\ <constants>
\\ <constant name="JOY_BUTTON_A" value="0" enum="JoyButton">Bottom action button.</constant>
\\ <constant name="JOY_BUTTON_B" value="1" enum="JoyButton">Right action button.</constant>
\\ </constants>
\\</class>
;

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();
Expand Down Expand Up @@ -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 =
\\<?xml version="1.0" encoding="UTF-8" ?>
\\<class name="Node" inherits="Object">
\\ <brief_description>Base class.</brief_description>
\\ <description>Base node class.</description>
\\</class>
,
});

var arena = std.heap.ArenaAllocator.init(allocator);
Expand Down
71 changes: 69 additions & 2 deletions src/cache.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down
Loading