// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
// Adapted from https://github.com/eiriktsarpalis/PolyType/blob/main/src/PolyType.Roslyn/SourceWriter.cs
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Microsoft.CodeAnalysis.Text;
namespace Product.Analyzers;
///
/// A utility class for generating consistently indented source code.
///
internal sealed class SourceWriter
{
// Fixed newlines make generated output deterministic across platforms.
private const string NewLine = "\r\n";
private readonly StringBuilder builder = new();
private int indentation;
///
/// Initializes a new instance of the class.
///
public SourceWriter()
: this('\t', 1)
{
}
///
/// Initializes a new instance of the class.
///
/// The whitespace character used for indentation.
/// The number of characters in each indentation level.
public SourceWriter(char indentationChar, int charsPerIndentation)
{
if (!char.IsWhiteSpace(indentationChar))
{
throw new ArgumentOutOfRangeException(nameof(indentationChar));
}
if (charsPerIndentation < 1)
{
throw new ArgumentOutOfRangeException(nameof(charsPerIndentation));
}
this.IndentationChar = indentationChar;
this.CharsPerIndentation = charsPerIndentation;
}
///
/// Gets the character used for indentation.
///
public char IndentationChar { get; }
///
/// Gets the number of characters per indentation level.
///
public int CharsPerIndentation { get; }
///
/// Gets the length of the generated source.
///
public int Length => this.builder.Length;
///
/// Gets or sets the current indentation level.
///
public int Indentation
{
get => this.indentation;
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
this.indentation = value;
}
}
///
/// Writes a character followed by a newline.
///
/// The character to write.
public void WriteLine(char value)
{
this.AddIndentation();
this.builder.Append(value);
this.builder.Append(NewLine);
}
///
/// Writes text followed by a newline.
///
/// The C# source to write.
/// Whether to suppress current indentation.
public void WriteLine(
[StringSyntax("c#-test")] string text,
bool disableIndentation = false)
{
bool isFinalLine;
ReadOnlySpan remainingText = text.AsSpan();
do
{
ReadOnlySpan nextLine = GetNextLine(ref remainingText, out isFinalLine);
if (!disableIndentation && this.indentation > 0)
{
this.AddIndentation();
}
this.AppendSpan(nextLine);
this.builder.Append(NewLine);
}
while (!isFinalLine);
}
///
/// Writes a newline.
///
public void WriteLine() => this.builder.Append(NewLine);
///
/// Returns the generated source text.
///
/// The generated source.
public SourceText ToSourceText()
{
if (this.builder.Length == 0)
{
throw new InvalidOperationException("Nothing was written.");
}
if (this.indentation != 0)
{
throw new InvalidOperationException($"Indentation level expected to be 0 but is {this.indentation}.");
}
return SourceText.From(this.builder.ToString(), Encoding.UTF8);
}
private static ReadOnlySpan GetNextLine(ref ReadOnlySpan remainingText, out bool isFinalLine)
{
if (remainingText.IsEmpty)
{
isFinalLine = true;
return default;
}
int lineLength = remainingText.IndexOf('\n');
ReadOnlySpan rest;
if (lineLength == -1)
{
lineLength = remainingText.Length;
isFinalLine = true;
rest = default;
}
else
{
rest = remainingText[(lineLength + 1)..];
isFinalLine = false;
}
if ((uint)lineLength > 0 && remainingText[lineLength - 1] == '\r')
{
lineLength--;
}
ReadOnlySpan next = remainingText[..lineLength];
remainingText = rest;
return next;
}
private void AddIndentation()
=> this.builder.Append(this.IndentationChar, this.CharsPerIndentation * this.indentation);
private void AppendSpan(ReadOnlySpan span)
=> this.builder.Append(span.ToString());
}