Scrollable dump cell

JohnGoldsmith
edited March 4 in General

I love Util.SyntaxColorText.

I often use it as a reportable property as below, but is it possible to make the cell that contains the dumped json scrollable so that very long json is contained?

void Main()
{
    var json = """
    {
        "name": "John"
    }
    """;

    new Thing()
    {
        SomeProperty = "Something",
        RawJson = Util.SyntaxColorText(json.ToString(), SyntaxLanguageStyle.Json, autoFormat: true)
    }.Dump();
}

public class Thing
{
    public required string SomeProperty { get; set; }
    public required LINQPad.SyntaxColoredText RawJson { get; set; }
}

Could there be some kind of LINQPad class like ScrollCell, something like this:

void Main()
{
    var json = """
    {
        "name": "John"
    }
    """;

    new Thing()
    {
        SomeProperty = "Something",
        RawJson = new ScrollCell
        {
            VisibleLines = 3,
            Content = Util.SyntaxColorText(json.ToString(), SyntaxLanguageStyle.Json, autoFormat: true)
        }
    }.Dump();
}

public class Thing
{
    public required string SomeProperty { get; set; }
    public required ScrollCell RawJson { get; set; }
}


public class ScrollCell
{
    public int VisibleLines { get; set; }
    public object Content { get; set; }
}

Best Answer

  • JoeAlbahari
    Answer ✓

    The easiest way to do this is with a DumpContainer:

    new DumpContainer (code) { Style = "max-height: 200px; overflow:auto" }.Dump();
    

    (Util.WithStyle should also work, although it doesn't right now due to a bug/limitation when wrapping block elements. I'll see whether this is easy to fix.)

    For a more fancy solution, you could create a custom Control:

    using LINQPad.Controls;
    
    class ScrollableControl : Control
    {
        public string MaxHeight { get => Styles["max-height"]; set => Styles["max-height"] = value; }
    
        public ScrollableControl (Control child, string maxHeight) : base ("div", child)
        {
            Styles["overflow"] = "auto";
            MaxHeight = maxHeight;
        }
    
        public ScrollableControl (object content, string maxHeight)
            : this (new DumpContainer (content), maxHeight) { }
    
        public ScrollableControl WithBorder (string border = "solid 1pt #777")
        {
            Styles["border"] = border;
            return this;
        }
    
        public ScrollableControl WithPadding (string padding = "3pt")
        {
            Styles["padding"] = padding;
            return this;
        }
    }
    

    Note that you can ask AI to write such wrappers. The recent beta actually includes additional training for creating custom controls, so now it's pretty reliable. Press Ctrl+I and ask it, Write a LINQPad control that makes SyntaxColorText or another control scrollable.

Answers

  • That's great. Thanks very much Joe. I hadn't considered having a DumpContainer in a cell.
    Will try out the beta as extra controls help has been on my wish list.

  • I created a wrapper for this purpose, but I like Joe's approach to it deriving from Control. Might switch over to something similar. Hopefully gives you other ideas on how to construct custom controls.

        public interface IFixedDumpContainer
        {
            Control Control { get; }
            DumpContainer Container { get; }
        }
        public static class UtilExtensions
        {
            extension(Util)
            {
                public static IFixedDumpContainer FixedDumpContainer(Action<Control>? configure = default) => new FixedDumpContainerImpl(null, configure);
                public static IFixedDumpContainer FixedDumpContainer(object initialContent, Action<Control>? configure = default) => new FixedDumpContainerImpl(initialContent, configure);
            }
    
            private class FixedDumpContainerImpl : IFixedDumpContainer
            {
                const string DefaultMaxHeight = "360px";
                const string DefaultMinWidth = "50%";
    
                public FixedDumpContainerImpl(object? initialContent, Action<Control>? configureControl)
                {
                    var dc = Container = initialContent is not null ? new(initialContent) : new();
                    var control = Control = Container.ToControl();
                    control.Styles["display"] = "inline-block";
                    control.Styles["overflow"] = "auto";
                    configureControl?.Invoke(control);
                    if (control.Styles.All(x => !x.Key.Contains("height", StringComparison.OrdinalIgnoreCase)))
                        control.Styles["max-height"] = DefaultMaxHeight;
                    if (control.Styles.All(x => !x.Key.Contains("width", StringComparison.OrdinalIgnoreCase)))
                        control.Styles["min-width"] = DefaultMinWidth;
                }
                public Control Control { get; }
                public DumpContainer Container { get; }
    
                object ToDump() => new Control(Control);
            }
        }
    
  • Hi Jeff,

    Thanks very much for this. I'd just started on adapting Joe's approach but it interesting to see other appoaches too.

    I ended up going down a slightly more 'text' specific route and came up with (with some ai help) this:

    #load ".\ControlsEx"
    
    void Main()
    {
        var json = File.ReadAllText(UtilEx.Paths.Desktop("Sample.json"));
    
        new Thing()
        {
            SomeProperty = "Something",
            RawJson = new ScrollableTextControl(Util.SyntaxColorText(json.ToString(), SyntaxLanguageStyle.Json, autoFormat: true), 
                                            visibleLines: 10)
        }.Dump();
    }
    
    public class Thing
    {
        public required string SomeProperty { get; set; }
        public required ScrollableTextControl RawJson { get; set; }
    }
    
    
    
    
    public class ScrollableTextControl : Control
    {
        class Attributes
        {
            internal const string Expanded = "data-expanded";
            internal const string ConstrainedHeight = "data-constrained-height";
        }
    
        private (string Top, string Right, string Bottom, string Left) _padding;
    
    
        private ScrollableTextControl(Control child, int visibleLines) : base("div")
        {
            VisualTree.Add(ExpandBtnControl = new Control("a", "Expand")
            {
                Styles =
                {
                    ["grid-row"] = "1",
                    ["grid-column"] = "2",
                    ["background"] = "#DAEAFA",
                    ["color"] = "black",
                    ["padding"] = "0.5em 0.75em",
                    ["margin"] = "0.25em 1.75em 0 0",
                    ["border-radius"] = "0.25em",
                    ["font-size"] = "0.9em",
                    ["text-decoration"] = "none",
                    ["opacity"] = "0.8",
                    ["cursor"] = "pointer",
                    ["z-index"] = "1",
                    ["user-select"] = "none"
                },
                HtmlElement =
                {
                    ["href"] = "#",
                    ["title"] = "Expand to full height",
                    ["onclick"] = $$"""
                        var p=this.parentElement;
                        var exp=p.getAttribute('{{Attributes.Expanded}}')==='true';
                        if(exp){
                            p.style.maxHeight=p.getAttribute('{{Attributes.ConstrainedHeight}}');
                            this.textContent='Expand';
                            this.title='Expand to full height';
                            p.setAttribute('{{Attributes.Expanded}}','false');
                        }
                        else{
                            p.style.maxHeight='none';
                            this.textContent='Collapse';
                            this.title='Collapse to constrained height';
                            p.setAttribute('{{Attributes.Expanded}}','true');
                        }
                        return false;
                        """
                }
            });
    
            VisualTree.Add(ContentWrapperControl = new Control("div", child)
            {
                Styles =
                {
                    ["grid-row"] = "1 / span 2",
                    ["grid-column"] = "1 / span 2",
                    ["overflow"] = "auto",
                    ["scrollbar-width"] = "auto",
                    ["scrollbar-gutter"] = "stable",
                    ["max-height"] = "inherit"
                }
            });
    
    
            Styles["display"] = "grid";
            Styles["grid-template-columns"] = "1fr auto";  // right col = button width only
            Styles["grid-template-rows"] = "auto 1fr";     // top row = button height only
            Styles["max-width"] = "60em";
            Padding = "0em 0em 0em 0em";
            HtmlElement[Attributes.Expanded] = "false";
            VisibleLines = visibleLines;        
        }
    
    
        public ScrollableTextControl(string content, int visibleLines) : this(new DumpContainer(content), visibleLines)
        {
            TotalLineCount = content.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None).Length;
        }
    
        public ScrollableTextControl(LINQPad.SyntaxColoredText content, int visibleLines) : this(new DumpContainer(content), visibleLines)
        {
            TotalLineCount = content.Text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None).Length;
        }
    
    
        public readonly Control ExpandBtnControl;
        public readonly Control ContentWrapperControl;
    
    
        public string Padding
        {
            get => Styles["padding"];
            set
            {
                _padding = ControlsEx.Css.ParsePaddingComponents(value);
                Styles["padding"] = value;
            }
        }
    
    
        private int _visibleLines;
        public int VisibleLines
        {
            get => _visibleLines;
            set
            {
                _visibleLines = value;
                UpdateLayout();
            }
        }
    
    
        private int _totalLineCount;
        public int TotalLineCount
        {
            get => _totalLineCount;
            private set
            {
                _totalLineCount = value;
                UpdateLayout();
            }
        }
    
    
        private void UpdateLayout()
        {
            if (_visibleLines == 0)
            {
                this.Visible = false;
            }
            else if (_visibleLines < 0 || _totalLineCount <= _visibleLines)
            {
                Styles["max-height"] = "none";
                ExpandBtnControl.Visible = false;
            }
            else
            {
                this.Visible = true;
                ExpandBtnControl.Visible = true;
                var constrained = $"calc(({_visibleLines} * 1lh) - {_padding.Bottom})";
                Styles["max-height"] = constrained;
                HtmlElement[Attributes.ConstrainedHeight] = constrained;
            }
        }
    }
    

    ControlsEx has this:

    public static class ControlsEx
    {
        public static class Css
        {
            public static (string top, string right, string bottom, string left) ParsePaddingComponents(string padding)
                => ParseBoxShorthand(padding);
    
            public static (string top, string right, string bottom, string left) ParseMarginComponents(string margin)
                => ParseBoxShorthand(margin);
    
            private static (string top, string right, string bottom, string left) ParseBoxShorthand(string value)
            {
                var parts = value.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
                return parts.Length switch
                {
                    1 => (parts[0], parts[0], parts[0], parts[0]),
                    2 => (parts[0], parts[1], parts[0], parts[1]),
                    3 => (parts[0], parts[1], parts[2], parts[1]),
                    4 => (parts[0], parts[1], parts[2], parts[3]),
                    _ => throw new ArgumentException($"Invalid CSS shorthand value: '{value}'")
                };
            }
        }
    }
    

    The output looks like this:


    Anyway, thanks again to you both.