Runtime: System.Console should support "Indentation Levels"

Created on 31 Aug 2017  ·  25Comments  ·  Source: dotnet/runtime

Rationale

It is often desired to print text that is properly indented to make it easy to read. However, in order to track indentation across method calls you have to either implement some wrapper class or pass the indentation level around.

As such, System.Console should be extended to natively support indentation levels in a similar manner to Sytem.Diagnostics.Debug

Proposal

System.Console should be extended to support the following APIs:

public static partial class Console
{
    // The indentation character, defaults to ' ' (space)
    public static char IndentChar { get; set; }

    // The indentation level, defaults to and will not decrease below 0
    public static int IndentLevel { get; set; }

    // The number of times `IndentChar` is repeated per `IndentLevel`, defaults to 4
    public static int IndentSize { get; set; }

    // Increases `IndentLevel` by one
    public static void Indent();

    // Decreases `IdentLevel` by one
    public static void Unindent();
}

Then, there are two sub-proposals:

Proposal A

Additionally expose a public static void WriteIndented() and public static void WriteLineIndented(), each with all of the corresponding overloads that Console.Write and Console.WriteLine have.

Proposal B

System.Console should internally track when the last WriteLine occurred and only prefix the indentation on the next Write or WriteLine.

This may end up being confusing to some users initially, but reduces the overall new APIs considerably.

Design Discussion api-needs-work area-System.Console

Most helpful comment

Likely the latter.

All 25 comments

Isn't this more of a TextWriter functionality than Console functionality?

I don't think Console or even the Out writer is the right level of abstraction for this. I've gone down this path and seen it go horribly wrong when you start doing async stuff. I'd prefer a context object wrapping the out writer– pretty much what you started out by saying you didn't want.

@AtsushiKan, yes, it is probably better to expose directly on something like TextWriter and make it easy to consume from System.Console.

@jnm2, This is (honestly) the simplest design I came up with. Yes, there are many things that could go wrong with async stuff (especially if you are not exclusively using WriteLine), and there is likely a better design to support that.

However, this (at a base level), is one of those common "utility" code wrappers that people keep implementing again and again (I want to output properly indented text, CoreFX doesn't support it, guess I'll write my own). As such, I think it is something that should be supported, at some level, by the core framework.

I don't mind it being supported by the framework, but people should start out using the correct abstraction or the minute they have more than one thread in play, they'll have to tear it all out and write their own indentation context “wrapper.”

Why is the existing IndentedTextWriter insufficient?
C# var writer = new IndentedTextWriter(Console.Out);

I didn't know it existed, but the fact that it's in System.​Code​Dom.​Compiler leads you to think it's somehow coupled to compilation in a way that didn't allow you to put it in a more generic namespace.

The namespace is poor. I don't believe that's a good enough reason to duplicate the functionality elsewhere, though.

@stephentoub, would it be unreasonable to have:

namespace System.IO
{
    public class IndentedTextWriter : System.CodeDom.Compiler.IndentedTextWriter
    {
        // This type explicitly has no implementation and merely exists so that the existing
        // IndentedTextWriter type can be exposed in a more logical location.
    }
}

Could the wrapper support using (indentedWriter.Indentation()) or something similar so that we don't leave try/finally everywhere?

This is not the first time I've wished for robust type forwarding to a new name and namespace and not just to a new assembly.

@tannergooding That's https://github.com/dotnet/csharplang/issues/410 if you want to do something about it 😍

would it be unreasonable to have

Even in different namespaces we try hard to avoid using the same type names, as it then causes problems if you import both namespaces.

My $.02, I also don't think it's worthwhile to do this just for discovery purposes.

@jnm2, yeah, I want assembly level aliases for a lot of reasons (really useful for Interop code as well).

I'm pretty sure something like that exists (kind-of) at the UWP layer (I know there is mappings from IIterable<T> to IEnumerable<T>, for example).

If only it were general purpose.

If only you knew some folks...

I may eat lunch with half the compiler team every day, but it doesn't mean I can convince them to implement something ahead of whatever schedule or priorities they already have (especially not without it going through LDM, which takes forever) 😉

Really it just means I can discuss why half my ideas don't make sense before putting it on the internet 😄

Yeah, I figured. And you'd have to get the CLR folks on board too 😁

One aspect to consider that perhaps makes indenting a more direct concern of the console is the width of the console. You probably want wrapped text to indent as well. To do that you have to respect the width of the console. Also when it changes, you probably want at least new writes to continue to indent correctly. I could see this being implemented as a wrapper of Console, but probably not in the base library.

So, maybe another idea for CoreFX Extensions (if that ever becomes a thing). Do we have a label to track such things?

We do not. @karelz maybe area-possibleextensions ?

To do that you have to respect the width of the console.

People's console windows reflow the text and there is no way to write a blocking resize handler. The only reasonable way to do this that I can find is to act like msbuild and check the width when the process starts and pretend it never changes.

check the width when the process starts and pretend it never changes

Or check it during every Write operation and support dynamic wrapping.

Of course, you're also into the hard problem of defining what a good wrapping point is. Oh, and don't forget to trim spaces so the console reflow doesn't trip. And sometimes WriteLine needs to leave the line out, since the console might do it as part of the cursor movement. And hopefully the user is well behaved and doesn't touch the keyboard, or all bets are off.

I've tried to make various versions of this over the years, and IndentedTextWriter usually ends up being about the right mix of useful and dumb. Anything smarter just makes me angry when it's wrong.

We have CoreFxExtension issue where you can add the idea: dotnet/runtime#22228 (I'd recommend to finish the discussion here first to get consensus if it is a good idea - I tend to agree with @bartonjs that IndentedTextWriter is probably the best middle-ground).

Or check it during every Write operation and support dynamic wrapping.

Of course, you're also into the hard problem of defining what a good wrapping point is. Oh, and don't forget to trim spaces so the console reflow doesn't trip. And sometimes WriteLine needs to leave the line out, since the console might do it as part of the cursor movement. And hopefully the user is well behaved and doesn't touch the keyboard, or all bets are off.

Like I said, there's no blocking resize handler, so no way to synchronize what you're writing with what the actual size of the window is. It gets awful pretty fast during a drag. I sunk days into this, which is why I called what msbuild does the only reasonable solution.

@tannergooding @bartonjs should the design discussion continue, or should we make the decision of staying with IndentedTextWriter as the preferred option to add indentation?

Likely the latter.

Was this page helpful?
0 / 5 - 0 ratings