diff --git a/src/snapshot_json.zig b/src/snapshot_json.zig index 36d41fc..f6760b3 100644 --- a/src/snapshot_json.zig +++ b/src/snapshot_json.zig @@ -11,19 +11,15 @@ pub fn buildSnapshot(explorer: *Explorer, store: *Store, alloc: std.mem.Allocato try w.writeAll("{"); try w.print("\"seq\":{d},", .{store.currentSeq()}); - const tree = try explorer.getTree(alloc, false); - defer alloc.free(tree); - try w.writeAll("\"tree\":\""); - try writeJsonEscaped(alloc, &buf, tree); - try w.writeAll("\","); - - try w.writeAll("\"outlines\":{"); { explorer.mu.lockShared(); defer explorer.mu.unlockShared(); + try buf.ensureTotalCapacity(alloc, roughSnapshotCapacity(explorer.outlines.count())); + var paths: std.ArrayList([]const u8) = .empty; defer paths.deinit(alloc); + try paths.ensureTotalCapacity(alloc, explorer.outlines.count()); var iter = explorer.outlines.iterator(); while (iter.next()) |entry| { try paths.append(alloc, entry.key_ptr.*); @@ -34,6 +30,11 @@ pub fn buildSnapshot(explorer: *Explorer, store: *Store, alloc: std.mem.Allocato } }.lessThan); + try w.writeAll("\"tree\":\""); + try writeTreeJsonEscaped(alloc, &buf, explorer, paths.items); + try w.writeAll("\","); + + try w.writeAll("\"outlines\":{"); for (paths.items, 0..) |path, pi| { if (pi > 0) try w.writeAll(","); try w.writeAll("\""); @@ -67,37 +68,13 @@ pub fn buildSnapshot(explorer: *Explorer, store: *Store, alloc: std.mem.Allocato } try w.writeAll("]}"); } - } - try w.writeAll("},"); - - try w.writeAll("\"symbol_index\":{"); - { - explorer.mu.lockShared(); - defer explorer.mu.unlockShared(); - - var sym_map = std.StringHashMap(std.ArrayList(SymEntry)).init(alloc); - defer { - var si = sym_map.iterator(); - while (si.next()) |e| e.value_ptr.deinit(alloc); - sym_map.deinit(); - } - - var oiter = explorer.outlines.iterator(); - while (oiter.next()) |entry| { - for (entry.value_ptr.symbols.items) |sym| { - const gop = try sym_map.getOrPut(sym.name); - if (!gop.found_existing) gop.value_ptr.* = .empty; - try gop.value_ptr.append(alloc, .{ - .path = entry.key_ptr.*, - .line = sym.line_start, - .kind = sym.kind, - }); - } - } + try w.writeAll("},"); + try w.writeAll("\"symbol_index\":{"); var sym_keys: std.ArrayList([]const u8) = .empty; defer sym_keys.deinit(alloc); - var ski = sym_map.iterator(); + try sym_keys.ensureTotalCapacity(alloc, explorer.symbol_index.count()); + var ski = explorer.symbol_index.iterator(); while (ski.next()) |e| try sym_keys.append(alloc, e.key_ptr.*); std.mem.sort([]const u8, sym_keys.items, {}, struct { fn lessThan(_: void, a: []const u8, b: []const u8) bool { @@ -110,27 +87,23 @@ pub fn buildSnapshot(explorer: *Explorer, store: *Store, alloc: std.mem.Allocato try w.writeAll("\""); try writeJsonEscaped(alloc, &buf, name); try w.writeAll("\":["); - const locs = sym_map.get(name) orelse continue; + const locs = explorer.symbol_index.get(name) orelse continue; for (locs.items, 0..) |loc, li| { if (li > 0) try w.writeAll(","); try w.writeAll("{\"path\":\""); try writeJsonEscaped(alloc, &buf, loc.path); try w.print("\",\"line\":{d},\"kind\":\"{s}\"}}", .{ - loc.line, @tagName(loc.kind), + loc.line_start, @tagName(loc.kind), }); } try w.writeAll("]"); } - } - try w.writeAll("},"); - - try w.writeAll("\"dep_graph\":{"); - { - explorer.mu.lockShared(); - defer explorer.mu.unlockShared(); + try w.writeAll("},"); + try w.writeAll("\"dep_graph\":{"); var dep_keys: std.ArrayList([]const u8) = .empty; defer dep_keys.deinit(alloc); + try dep_keys.ensureTotalCapacity(alloc, explorer.dep_graph.count()); var diter = explorer.dep_graph.iterator(); while (diter.next()) |e| try dep_keys.append(alloc, e.key_ptr.*); std.mem.sort([]const u8, dep_keys.items, {}, struct { @@ -153,33 +126,93 @@ pub fn buildSnapshot(explorer: *Explorer, store: *Store, alloc: std.mem.Allocato } try w.writeAll("]"); } + try w.writeAll("}"); } - try w.writeAll("}}"); + try w.writeAll("}"); return buf.toOwnedSlice(alloc); } -const SymEntry = struct { - path: []const u8, - line: u32, - kind: @import("explore.zig").SymbolKind, -}; +fn roughSnapshotCapacity(file_count: usize) usize { + const min_capacity: usize = 64 * 1024; + const max_capacity: usize = 8 * 1024 * 1024; + const per_file: usize = 32 * 1024; + if (file_count == 0) return min_capacity; + if (file_count > max_capacity / per_file) return max_capacity; + return @max(min_capacity, file_count * per_file); +} + +fn writeTreeJsonEscaped(alloc: std.mem.Allocator, out: *std.ArrayList(u8), explorer: *Explorer, paths: []const []const u8) !void { + const w = cio.listWriter(out, alloc); + var seen_dirs = std.StringHashMap(void).init(alloc); + defer seen_dirs.deinit(); + + for (paths) |path| { + const outline = explorer.outlines.get(path) orelse continue; + + var prefix_end: usize = 0; + while (std.mem.indexOfScalarPos(u8, path, prefix_end, '/')) |sep| { + const dir = path[0 .. sep + 1]; + if (!seen_dirs.contains(dir)) { + try seen_dirs.put(dir, {}); + const depth = std.mem.count(u8, dir[0..sep], "/"); + for (0..depth) |_| try w.writeAll(" "); + const dir_name = path[if (depth > 0) std.mem.lastIndexOfScalar(u8, dir[0..sep], '/').? + 1 else 0..sep]; + try writeJsonEscaped(alloc, out, dir_name); + try w.writeAll("/\\n"); + } + prefix_end = sep + 1; + } + + const depth = std.mem.count(u8, path, "/"); + for (0..depth) |_| try w.writeAll(" "); + const basename = if (std.mem.lastIndexOfScalar(u8, path, '/')) |pos| path[pos + 1 ..] else path; + try writeJsonEscaped(alloc, out, basename); + try w.print(" {s} {d}L {d} sym\\n", .{ + @tagName(outline.language), + outline.line_count, + outline.symbols.items.len, + }); + } +} fn writeJsonEscaped(alloc: std.mem.Allocator, out: *std.ArrayList(u8), s: []const u8) !void { - for (s) |c| { + var start: usize = 0; + for (s, 0..) |c, i| { switch (c) { - '"' => try out.appendSlice(alloc, "\\\""), - '\\' => try out.appendSlice(alloc, "\\\\"), - '\n' => try out.appendSlice(alloc, "\\n"), - '\r' => try out.appendSlice(alloc, "\\r"), - '\t' => try out.appendSlice(alloc, "\\t"), + '"' => { + if (i > start) try out.appendSlice(alloc, s[start..i]); + try out.appendSlice(alloc, "\\\""); + start = i + 1; + }, + '\\' => { + if (i > start) try out.appendSlice(alloc, s[start..i]); + try out.appendSlice(alloc, "\\\\"); + start = i + 1; + }, + '\n' => { + if (i > start) try out.appendSlice(alloc, s[start..i]); + try out.appendSlice(alloc, "\\n"); + start = i + 1; + }, + '\r' => { + if (i > start) try out.appendSlice(alloc, s[start..i]); + try out.appendSlice(alloc, "\\r"); + start = i + 1; + }, + '\t' => { + if (i > start) try out.appendSlice(alloc, s[start..i]); + try out.appendSlice(alloc, "\\t"); + start = i + 1; + }, else => if (c < 0x20) { + if (i > start) try out.appendSlice(alloc, s[start..i]); const hex = "0123456789abcdef"; const esc = [6]u8{ '\\', 'u', '0', '0', hex[c >> 4], hex[c & 0x0f] }; try out.appendSlice(alloc, &esc); - } else { - try out.append(alloc, c); + start = i + 1; }, } } + if (start < s.len) try out.appendSlice(alloc, s[start..]); } diff --git a/src/tests.zig b/src/tests.zig index 52e723c..9d5d2ba 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -1647,6 +1647,14 @@ test "snapshot_json: snapshot builds and is valid JSON" { try testing.expect(parsed.value.object.contains("outlines")); try testing.expect(parsed.value.object.contains("symbol_index")); try testing.expect(parsed.value.object.contains("dep_graph")); + + const tree = parsed.value.object.get("tree").?.string; + try testing.expect(std.mem.indexOf(u8, tree, "src/") != null); + try testing.expect(std.mem.indexOf(u8, tree, "main.zig") != null); + + const symbol_index = parsed.value.object.get("symbol_index").?.object; + try testing.expect(symbol_index.contains("main")); + try testing.expect(symbol_index.contains("version")); } // ── Deep copy correctness tests ─────────────────────────────