import Combine import SwiftUI import LumaCore struct JSInspectValueView: View { let value: JSInspectValue let sessionID: UUID let engine: Engine let selection: Binding private let circularTargets: Set @StateObject private var anchorStore = CircularAnchorStore() init( value: JSInspectValue, sessionID: UUID, engine: Engine, selection: Binding ) { self.value = value self.sessionID = sessionID self.engine = engine self.selection = selection self.circularTargets = JSInspectValueView.collectCircularTargets(in: value) } var body: some View { JSInspectNodeView( value: value, depth: 0, sessionID: sessionID, engine: engine, selection: selection ) .environment(\.circularTargets, circularTargets) .environmentObject(anchorStore) .textSelection(.enabled) .errorPopoverHost() } private static func collectCircularTargets(in value: JSInspectValue) -> Set { var ids = Set() func walk(_ v: JSInspectValue) { switch v { case .object(_, let props): for p in props { walk(p.key) walk(p.value) } case .array(_, let elements): for e in elements { walk(e) } case .map(_, let entries): for e in entries { walk(e.key) walk(e.value) } case .set(_, let elements): for e in elements { walk(e) } case .circular(let id): ids.insert(id) default: break } } walk(value) return ids } } private struct JSInspectNodeView: View { static let chunkSize = 5 let value: JSInspectValue let depth: Int let sessionID: UUID let engine: Engine let selection: Binding @State private var isExpanded: Bool @State private var childLimit: Int = Self.chunkSize @Environment(\.replExpandAction) private var onExpand @Environment(\.errorPresenter) private var errorPresenter @Environment(\.circularTargets) private var circularTargets @EnvironmentObject private var anchorStore: CircularAnchorStore init( value: JSInspectValue, depth: Int, sessionID: UUID, engine: Engine, selection: Binding ) { self.value = value self.depth = depth self.sessionID = sessionID self.engine = engine self.selection = selection self._isExpanded = State(initialValue: depth == 0) } var body: some View { switch value { case .object(_, let props): objectView(props) case .array(_, let elements): arrayView(elements) case .map(_, let entries): mapView(entries) case .set(_, let elements): setView(elements) default: leafView(value) } } private func objectView(_ props: [JSInspectValue.Property]) -> some View { DisclosureGroup( isExpanded: $isExpanded, content: { LazyVStack(alignment: .leading, spacing: 2) { ForEach(0.. childLimit { showMoreButton(total: props.count, noun: "properties") } } .padding(.leading, 12) }, label: { disclosureLabel( typeText: "Object{\(props.count)}\(anchorSuffix())", preview: isExpanded ? nil : inlinePreview(value) ) } ) } private func showMoreButton(total: Int, noun: String) -> some View { let remaining = total - childLimit let next = min(Self.chunkSize, remaining) return Button("Show \(next) more (\(remaining) \(noun) left)…") { childLimit = min(childLimit + Self.chunkSize, total) onExpand() } .platformLinkButtonStyle() } private func disclosureLabel(typeText: String, preview: String?) -> some View { var attributed = AttributedString(typeText) attributed.foregroundColor = Color.jsTypeLabel if let preview { var previewPart = AttributedString(" \(preview)") previewPart.foregroundColor = .secondary attributed.append(previewPart) } return Text(attributed) .lineLimit(1) .truncationMode(.tail) } private func arrayView(_ elements: [JSInspectValue]) -> some View { DisclosureGroup( isExpanded: $isExpanded, content: { LazyVStack(alignment: .leading, spacing: 2) { ForEach(0.. childLimit { showMoreButton(total: elements.count, noun: "items") } } .padding(.leading, 12) }, label: { disclosureLabel( typeText: "Array[\(elements.count)]\(anchorSuffix())", preview: isExpanded ? nil : inlinePreview(value) ) } ) } private func mapView(_ entries: [JSInspectValue.Property]) -> some View { DisclosureGroup( isExpanded: $isExpanded, content: { LazyVStack(alignment: .leading, spacing: 2) { ForEach(0.. childLimit { showMoreButton(total: entries.count, noun: "entries") } } .padding(.leading, 12) }, label: { disclosureLabel( typeText: "Map{\(entries.count)}\(anchorSuffix())", preview: isExpanded ? nil : inlinePreview(value) ) } ) } private func setView(_ elements: [JSInspectValue]) -> some View { DisclosureGroup( isExpanded: $isExpanded, content: { LazyVStack(alignment: .leading, spacing: 2) { ForEach(0.. childLimit { showMoreButton(total: elements.count, noun: "items") } } .padding(.leading, 12) }, label: { disclosureLabel( typeText: "Set{\(elements.count)}\(anchorSuffix())", preview: isExpanded ? nil : inlinePreview(value) ) } ) } @ViewBuilder private func leafView(_ value: JSInspectValue) -> some View { switch value { case .bytes(let bytes): VStack(alignment: .leading, spacing: 4) { Text("Bytes(\(bytes.kind.rawValue)[\(bytes.data.count)])") .foregroundStyle(.mint) HexView(data: bytes.data) } default: if case .nativePointer = value, let addr = value.nativePointerAddress { Text(value.prettyAttributedDescription()) .fixedSize(horizontal: false, vertical: true) .pointerActions( engine: engine, sessionID: sessionID, value: String(format: "0x%llx", addr), address: addr, selection: selection ) } else { Text(value.prettyAttributedDescription()) .fixedSize(horizontal: false, vertical: true) } } } private func containerId() -> Int? { switch value { case .object(let id, _), .array(let id, _), .map(let id, _), .set(let id, _): return id default: return nil } } private func anchorSuffix() -> String { guard let id = containerId() else { return "" } guard circularTargets.contains(id) else { return "" } if !anchorStore.anchoredIds.contains(id) { anchorStore.anchoredIds.insert(id) return " *\(id)" } return "" } private func inlinePreview(_ value: JSInspectValue) -> String? { switch value { case .object(_, let props): let preview = props.prefix(3) .map { "\($0.displayKey): \($0.value.inlineDescription)" } .joined(separator: ", ") return props.isEmpty ? nil : "{\(preview)\(props.count > 3 ? ", …" : "")}" case .array(_, let elements): let preview = elements.prefix(3) .map { $0.inlineDescription } .joined(separator: ", ") return elements.isEmpty ? nil : "[\(preview)\(elements.count > 3 ? ", …" : "")]" case .map(_, let entries): let preview = entries.prefix(3) .map { "\($0.key.inlineDescription) => \($0.value.inlineDescription)" } .joined(separator: ", ") return entries.isEmpty ? nil : "{\(preview)\(entries.count > 3 ? ", …" : "")}" case .set(_, let elements): let preview = elements.prefix(3) .map { $0.inlineDescription } .joined(separator: ", ") return elements.isEmpty ? nil : "{\(preview)\(elements.count > 3 ? ", …" : "")}" default: return nil } } } private final class CircularAnchorStore: ObservableObject { @Published var anchoredIds: Set = [] } extension EnvironmentValues { var circularTargets: Set { get { self[CircularTargetsKey.self] } set { self[CircularTargetsKey.self] = newValue } } } private struct CircularTargetsKey: EnvironmentKey { static let defaultValue: Set = [] }