diff --git a/touki.tests/Touki/Io/TextWriterExtensionsTests.cs b/touki.tests/Touki/Io/TextWriterExtensionsTests.cs new file mode 100644 index 0000000..ad95e8a --- /dev/null +++ b/touki.tests/Touki/Io/TextWriterExtensionsTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) 2025 Jeremy W Kuhne +// SPDX-License-Identifier: MIT +// See LICENSE file in the project root for full license information + +using System.Text; +using Touki.Text; + +namespace Touki.Io; + +public class TextWriterExtensionsTests +{ + [Fact] + public void Write_ReadOnlySpan_AppendsToStringWriter() + { + System.IO.StringWriter writer = new(); + ReadOnlySpan span = "Hello".AsSpan(); + + writer.Write(span); + + writer.ToString().Should().Be("Hello"); + } + + [Fact] + public void Write_ReadOnlySpan_Empty_DoesNothing() + { + System.IO.StringWriter writer = new(); + + writer.Write([]); + + writer.ToString().Should().BeEmpty(); + } + + [Fact] + public void WriteLine_ReadOnlySpan_AppendsAndAddsNewLine() + { + System.IO.StringWriter writer = new(); + ReadOnlySpan span = "Hello".AsSpan(); + + writer.WriteLine(span); + + writer.ToString().Should().Be($"Hello{Environment.NewLine}"); + } + + [Fact] + public void WriteLine_ReadOnlySpan_Empty_WritesOnlyNewLine() + { + System.IO.StringWriter writer = new(); + + writer.WriteLine([]); + + writer.ToString().Should().Be(Environment.NewLine); + } + + [Fact] + public void Write_StringSegment_WritesSegmentContent() + { + System.IO.StringWriter writer = new(); + StringSegment segment = new("Hello World", 6, 5); + + writer.Write(segment.AsSpan()); + + writer.ToString().Should().Be("World"); + } + + [Fact] + public void WriteLine_StringSegment_WritesSegmentContentAndNewLine() + { + System.IO.StringWriter writer = new(); + StringSegment segment = new("Hello World", 0, 5); + + writer.WriteLine(segment.AsSpan()); + + writer.ToString().Should().Be($"Hello{Environment.NewLine}"); + } + + [Fact] + public void WriteFormatted_InterpolatedString_AppendsToStreamWriter() + { + using MemoryStream stream = new(); + using System.IO.StreamWriter writer = new(stream, Encoding.UTF8, 1024, leaveOpen: true); + + string name = "Touki"; + int version = 42; + + writer.WriteFormatted($"Library: {name}, Version: {version}"); + writer.Flush(); + + stream.Position = 0; + using StreamReader reader = new(stream, Encoding.UTF8); + string result = reader.ReadToEnd(); + + result.Should().Be("Library: Touki, Version: 42"); + } + + [Fact] + public void WriteFormatted_EmptyBuilder_WritesNothing() + { + using MemoryStream stream = new(); + using System.IO.StreamWriter writer = new(stream, Encoding.UTF8, 1024, leaveOpen: true); + writer.Flush(); + long length = stream.Length; + + ValueStringBuilder builder = new(); + writer.WriteFormatted(ref builder); + writer.Flush(); + + stream.Length.Should().Be(length); + } + +#if NET + [Fact] + public void WriteFormatted_StringOverload_WritesLiteralWithoutBuilder() + { + System.IO.StringWriter writer = new(); + + writer.WriteFormatted("Hello"); + + writer.ToString().Should().Be("Hello"); + } +#endif +} diff --git a/touki.tests/Touki/SpanReaderEdgeCaseTests.cs b/touki.tests/Touki/SpanReaderEdgeCaseTests.cs index aac9e4c..6ea91f9 100644 --- a/touki.tests/Touki/SpanReaderEdgeCaseTests.cs +++ b/touki.tests/Touki/SpanReaderEdgeCaseTests.cs @@ -222,7 +222,7 @@ public void TryRead_Struct_ValidatesTypeSizeAlignment() { // Try to read a byte (1 byte) from ushort span (2-byte elements) // sizeof(byte) < sizeof(ushort) should trigger the exception - reader.TryRead(out byte _); + reader.TryRead(out byte _); Assert.Fail($"Expected {nameof(ArgumentException)}"); } catch (ArgumentException ex) @@ -240,7 +240,7 @@ public void TryRead_Struct_Count_ValidatesTypeSizeAlignment() try { // Try to read bytes from ushort span - should throw because byte size < ushort size - reader.TryRead(2, out ReadOnlySpan _); + reader.TryRead(2, out ReadOnlySpan _); Assert.Fail($"Expected {nameof(ArgumentException)}"); } catch (ArgumentException ex) @@ -449,7 +449,7 @@ public void TryRead_Struct_EdgeCaseSizes() SpanReader reader = new(span); // Test reading a 16-byte struct (should succeed) - bool result = reader.TryRead(out decimal _); + bool result = reader.TryRead(out decimal _); result.Should().BeTrue(); reader.Position.Should().Be(16); reader.End.Should().BeTrue(); @@ -469,7 +469,7 @@ public void TryRead_Struct_Count_EdgeCases() SpanReader reader = new(span); // Test reading exactly the maximum number of items that fit - bool result = reader.TryRead(4, out ReadOnlySpan values); // 4 * 4 = 16 bytes + bool result = reader.TryRead(4, out ReadOnlySpan values); // 4 * 4 = 16 bytes result.Should().BeTrue(); values.Length.Should().Be(4); reader.Position.Should().Be(16); @@ -478,7 +478,7 @@ public void TryRead_Struct_Count_EdgeCases() reader.Reset(); // Test reading one more than what fits - result = reader.TryRead(5, out values); // 5 * 4 = 20 bytes (too much) + result = reader.TryRead(5, out values); // 5 * 4 = 20 bytes (too much) result.Should().BeFalse(); values.Length.Should().Be(0); reader.Position.Should().Be(0); diff --git a/touki.tests/Touki/SpanReaderTests.cs b/touki.tests/Touki/SpanReaderTests.cs index c836f07..a0d1eb7 100644 --- a/touki.tests/Touki/SpanReaderTests.cs +++ b/touki.tests/Touki/SpanReaderTests.cs @@ -1218,7 +1218,7 @@ public void SpanReader_TryRead_Struct_LargerThanElementSize() SpanReader reader = new(span); // Test reading larger struct (ulong is 8 bytes) - reader.TryRead(out ulong value).Should().BeTrue(); + reader.TryRead(out ulong value).Should().BeTrue(); reader.Position.Should().Be(8); reader.End.Should().BeTrue(); diff --git a/touki.tests/Touki/StreamExtensionsTests.cs b/touki.tests/Touki/StreamExtensionsTests.cs index 699bb55..cf42f42 100644 --- a/touki.tests/Touki/StreamExtensionsTests.cs +++ b/touki.tests/Touki/StreamExtensionsTests.cs @@ -94,6 +94,18 @@ public void WriteFormatted_SimpleString_WritesToMemoryStream() result.Should().Be("Hello World!"); } +#if NET + [Fact] + public void WriteFormatted_StringOverload_WritesUtf16Bytes() + { + using MemoryStream stream = new(); + + stream.WriteFormatted("Hi"); + + stream.ToArray().Should().BeEquivalentTo(Encoding.Unicode.GetBytes("Hi")); + } +#endif + [Fact] public void WriteFormatted_EmptyBuilder_WritesNothing() { @@ -133,92 +145,6 @@ public void WriteFormatted_MultipleWrites_AppendToStream() result.Should().Be("First part. Second part."); } - [Fact] - public void WriteFormatted_StreamWriterUnicode_WritesCorrectly() - { - using MemoryStream stream = new(); - StreamWriter writer = new(stream, Encoding.Unicode); - -#pragma warning disable IDE0082 // 'typeof' can be converted to 'nameof' - writer.WriteFormatted($"Hello from {typeof(StreamWriter).Name}!"); -#pragma warning restore IDE0082 - writer.Flush(); - - stream.Position = 0; - StreamReader reader = new(stream, Encoding.Unicode); - string result = reader.ReadToEnd(); - - result.Should().Be("Hello from StreamWriter!"); - } - - [Fact] - public void WriteFormatted_StreamWriterUTF8_WritesCorrectly() - { - using MemoryStream stream = new(); - StreamWriter writer = new(stream, Encoding.UTF8); - - writer.WriteFormatted($"UTF-{8} Text"); - writer.Flush(); - - stream.Position = 0; - StreamReader reader = new(stream, Encoding.UTF8); - string result = reader.ReadToEnd(); - - result.Should().Be("UTF-8 Text"); - } - - [Fact] - public void WriteFormatted_StreamWriterInterpolatedValues_WritesCorrectly() - { - using MemoryStream stream = new(); - StreamWriter writer = new(stream, Encoding.UTF8); - - string name = "StreamWriter"; - int value = 123; - double pi = 3.14159; - - writer.WriteFormatted($"Name: {name}, Value: {value}, Pi: {pi:F2}"); - writer.Flush(); - - stream.Position = 0; - StreamReader reader = new(stream, Encoding.UTF8); - string result = reader.ReadToEnd(); - - result.Should().Be("Name: StreamWriter, Value: 123, Pi: 3.14"); - } - - [Fact] - public void WriteFormatted_StreamWriterMultipleWrites_AppendCorrectly() - { - using MemoryStream stream = new(); - StreamWriter writer = new(stream, Encoding.Unicode); - - writer.WriteFormatted($"Part one. "); - writer.WriteFormatted($"Part two."); - writer.Flush(); - - stream.Position = 0; - StreamReader reader = new(stream, Encoding.Unicode); - string result = reader.ReadToEnd(); - - result.Should().Be("Part one. Part two."); - } - - [Fact] - public void WriteFormatted_StreamWriterEmptyBuilder_WritesNothing() - { - using MemoryStream stream = new(); - StreamWriter writer = new(stream, Encoding.UTF8); - writer.Flush(); - long length = stream.Length; - - ValueStringBuilder builder = new(); - writer.WriteFormatted(ref builder); - writer.Flush(); - - stream.Length.Should().Be(length); - } - [Fact] public void Write_ReadOnlySpan_WritesToTextWriter() { diff --git a/touki/Framework/System/Number.NumberBuffer.cs b/touki/Framework/System/Number.NumberBuffer.cs index 166efd3..e25c2b9 100644 --- a/touki/Framework/System/Number.NumberBuffer.cs +++ b/touki/Framework/System/Number.NumberBuffer.cs @@ -5,8 +5,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Text; - namespace System; internal static partial class Number diff --git a/touki/Framework/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler.cs b/touki/Framework/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler.cs index 0027995..1de977b 100644 --- a/touki/Framework/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler.cs +++ b/touki/Framework/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler.cs @@ -8,7 +8,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Touki; -using Touki.Text; namespace System.Runtime.CompilerServices; diff --git a/touki/Framework/Touki/Exceptions/ThrowHelper.cs b/touki/Framework/Touki/Exceptions/ThrowHelper.cs index dfb5e7c..9f9e797 100644 --- a/touki/Framework/Touki/Exceptions/ThrowHelper.cs +++ b/touki/Framework/Touki/Exceptions/ThrowHelper.cs @@ -38,7 +38,6 @@ // multiple times for different instantiation. // -using System.Buffers; using System.IO; using System.Reflection; using System.Runtime.Serialization; diff --git a/touki/Framework/Touki/Io/StreamExtensions.cs b/touki/Framework/Touki/Io/StreamExtensions.cs deleted file mode 100644 index c2563b6..0000000 --- a/touki/Framework/Touki/Io/StreamExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2025 Jeremy W Kuhne -// SPDX-License-Identifier: MIT -// See LICENSE file in the project root for full license information - -using System.Buffers; -using System.IO; - -namespace Touki.Io; - -/// -/// Extension methods for . -/// -public static partial class StreamExtensions -{ - extension(TextWriter writer) - { - /// - /// Allows writing a to a . - /// - public void Write(ReadOnlySpan value) - { - if (value.Length > 0) - { - char[] buffer = ArrayPool.Shared.Rent(value.Length); - value.CopyTo(buffer); - writer.Write(buffer, 0, value.Length); - ArrayPool.Shared.Return(buffer); - } - } - - /// - /// Allows writing a to a . - /// - public void WriteLine(ReadOnlySpan value) - { - if (value.Length > 0) - { - char[] buffer = ArrayPool.Shared.Rent(value.Length); - value.CopyTo(buffer); - writer.Write(buffer, 0, value.Length); - ArrayPool.Shared.Return(buffer); - } - - writer.WriteLine(); - } - } -} diff --git a/touki/Framework/Touki/Io/TextWriterExtensions.cs b/touki/Framework/Touki/Io/TextWriterExtensions.cs new file mode 100644 index 0000000..0c6a137 --- /dev/null +++ b/touki/Framework/Touki/Io/TextWriterExtensions.cs @@ -0,0 +1,63 @@ +// Copyright (c) 2025 Jeremy W Kuhne +// SPDX-License-Identifier: MIT +// See LICENSE file in the project root for full license information + +namespace Touki.Io; + +/// +/// Extension methods for . +/// +public static partial class TextWriterExtensions +{ + extension(TextWriter writer) + { + /// + /// Allows writing a to a . + /// + public void Write(ReadOnlySpan value) + { + if (value.Length == 0) + { + return; + } + + if (writer is StringWriter stringWriter) + { + stringWriter.GetStringBuilder().AppendSpan(value); + return; + } + + // Fall back to renting a buffer + char[] buffer = ArrayPool.Shared.Rent(value.Length); + value.CopyTo(buffer); + writer.Write(buffer, 0, value.Length); + ArrayPool.Shared.Return(buffer); + } + + /// + /// Allows writing a to a . + /// + public void WriteLine(ReadOnlySpan value) + { + if (value.Length == 0) + { + writer.WriteLine(); + return; + } + + if (writer is StringWriter stringWriter) + { + stringWriter.GetStringBuilder().AppendSpan(value); + writer.WriteLine(); + return; + } + + char[] buffer = ArrayPool.Shared.Rent(value.Length); + value.CopyTo(buffer); + writer.Write(buffer, 0, value.Length); + ArrayPool.Shared.Return(buffer); + + writer.WriteLine(); + } + } +} diff --git a/touki/Framework/Touki/Text/ChunkEnumerator.cs b/touki/Framework/Touki/Text/ChunkEnumerator.cs index 0817fc1..3db72b9 100644 --- a/touki/Framework/Touki/Text/ChunkEnumerator.cs +++ b/touki/Framework/Touki/Text/ChunkEnumerator.cs @@ -8,7 +8,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; -using System.Text; namespace Touki.Text; diff --git a/touki/Framework/Touki/Text/StringBuilderExtensions.cs b/touki/Framework/Touki/Text/StringBuilderExtensions.cs index cb90644..ba58b68 100644 --- a/touki/Framework/Touki/Text/StringBuilderExtensions.cs +++ b/touki/Framework/Touki/Text/StringBuilderExtensions.cs @@ -2,8 +2,6 @@ // SPDX-License-Identifier: MIT // See LICENSE file in the project root for full license information -using System.Text; - namespace Touki.Text; /// diff --git a/touki/GlobalUsings.cs b/touki/GlobalUsings.cs index b53b201..1013b33 100644 --- a/touki/GlobalUsings.cs +++ b/touki/GlobalUsings.cs @@ -4,7 +4,9 @@ #pragma warning disable IDE0005 // Using directive is unnecessary. global using System; +global using System.Buffers; global using System.Collections.Generic; +global using System.ComponentModel; global using System.Diagnostics; global using System.Diagnostics.CodeAnalysis; global using System.Runtime.CompilerServices; @@ -28,10 +30,19 @@ global using IOException = System.IO.IOException; global using PathTooLongException = System.IO.PathTooLongException; global using Stream = System.IO.Stream; +global using StreamReader = System.IO.StreamReader; +global using StreamWriter = System.IO.StreamWriter; +global using StringWriter = System.IO.StringWriter; global using TextReader = System.IO.TextReader; +global using TextWriter = System.IO.TextWriter; global using Marshal = System.Runtime.InteropServices.Marshal; +// For some reason including all of System.Text causes XML doc generation to fail on .NET Framework builds. +global using StringBuilder = System.Text.StringBuilder; + global using Touki.Exceptions; +global using Touki.Text; +global using Touki.Io; #pragma warning restore IDE0005 // Using directive is unnecessary. diff --git a/touki/Touki/Buffers/BufferScope.cs b/touki/Touki/Buffers/BufferScope.cs index 0865a1c..59bd45b 100644 --- a/touki/Touki/Buffers/BufferScope.cs +++ b/touki/Touki/Buffers/BufferScope.cs @@ -2,8 +2,6 @@ // SPDX-License-Identifier: MIT // See LICENSE file in the project root for full license information -using System.Buffers; - namespace Touki; /// diff --git a/touki/Touki/Collections/ArrayPoolList.cs b/touki/Touki/Collections/ArrayPoolList.cs index 860ae53..949fdca 100644 --- a/touki/Touki/Collections/ArrayPoolList.cs +++ b/touki/Touki/Collections/ArrayPoolList.cs @@ -2,8 +2,6 @@ // SPDX-License-Identifier: MIT // See LICENSE file in the project root for full license information -using System.Buffers; - namespace Touki.Collections; /// diff --git a/touki/Touki/Collections/ContiguousList.cs b/touki/Touki/Collections/ContiguousList.cs index ab36c4f..a5db685 100644 --- a/touki/Touki/Collections/ContiguousList.cs +++ b/touki/Touki/Collections/ContiguousList.cs @@ -2,8 +2,6 @@ // SPDX-License-Identifier: MIT // See LICENSE file in the project root for full license information -using System.ComponentModel; - namespace Touki.Collections; /// diff --git a/touki/Touki/ComponentModel/ComponentBase.cs b/touki/Touki/ComponentModel/ComponentBase.cs index 0fad9d8..9a84ae6 100644 --- a/touki/Touki/ComponentModel/ComponentBase.cs +++ b/touki/Touki/ComponentModel/ComponentBase.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: MIT // See LICENSE file in the project root for full license information -using System.ComponentModel; using System.Threading; namespace Touki; diff --git a/touki/Touki/Io/StreamExtensions.cs b/touki/Touki/Io/StreamExtensions.cs index 3fca406..67c38c4 100644 --- a/touki/Touki/Io/StreamExtensions.cs +++ b/touki/Touki/Io/StreamExtensions.cs @@ -4,17 +4,13 @@ using System.Threading; using System.Threading.Tasks; -#if !NET -using System.IO; -#endif -using Touki.Text; namespace Touki.Io; /// /// Extension methods for . /// -public static partial class StreamExtensions +public static partial class TextWriterExtensions { /// The target stream. extension(Stream stream) @@ -68,7 +64,7 @@ public Task WriteAsync(ArraySegment buffer, CancellationToken cancellation : Task.CompletedTask; /// - /// Writes an interpolated string directly to a stream. + /// Writes an interpolated string directly to a . /// public void WriteFormatted(ref ValueStringBuilder builder) { @@ -78,33 +74,23 @@ public void WriteFormatted(ref ValueStringBuilder builder) builder.Clear(); } } - } - extension(StreamWriter writer) - { +#if NET /// - /// Writes an interpolated string directly to a . + /// Writes a string directly to a . /// - public void WriteFormatted(ref ValueStringBuilder builder) + /// + /// + /// Optimization overload that allows string literals to be used without creating a builder. + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public void WriteFormatted(string value) { - if (builder.Length > 0) - { - builder.CopyTo(writer); - builder.Clear(); - } + // While it would be nice to have this for .NET Framework, the only method we have on + // stream takes a byte[] buffer. We can't reinterpret the string as a byte[]. + stream.Write(MemoryMarshal.AsBytes(value.AsSpan())); } - } - - extension(TextWriter writer) - { - /// - /// Allows writing a to a . - /// - public void Write(StringSegment value) => writer.Write(value.AsSpan()); - - /// - /// Allows writing a to a . - /// - public void WriteLine(StringSegment value) => writer.WriteLine(value.AsSpan()); +#endif } } diff --git a/touki/Touki/Io/TextWriterExtensions.cs b/touki/Touki/Io/TextWriterExtensions.cs new file mode 100644 index 0000000..c23fa2f --- /dev/null +++ b/touki/Touki/Io/TextWriterExtensions.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2025 Jeremy W Kuhne +// SPDX-License-Identifier: MIT +// See LICENSE file in the project root for full license information + +namespace Touki.Io; + +/// +/// Extension methods for . +/// +public static partial class TextWriterExtensions +{ + extension(TextWriter writer) + { + /// + /// Allows writing a to a . + /// + public void Write(StringSegment value) => value.WriteTo(writer); + + /// + /// Allows writing a to a . + /// + public void WriteLine(StringSegment value) + { + value.WriteTo(writer); + writer.WriteLine(); + } + + /// + /// Writes an interpolated string directly to a . + /// + public void WriteFormatted(ref ValueStringBuilder builder) + { + if (builder.Length > 0) + { + builder.CopyTo(writer); + builder.Clear(); + } + } + +#if NET + /// + /// Writes a string directly to a . + /// + /// + /// + /// Optimization overload that allows string literals to be used without creating a builder. + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public void WriteFormatted(string value) + { + // While it would be nice to have this for .NET Framework, the only method we have on + // StreamWriter takes a char[] buffer. We can't reinterpret the string as a char[]. + writer.Write(value.AsSpan()); + } +#endif + } +} diff --git a/touki/Touki/Text/StringBuilderExtensions.cs b/touki/Touki/Text/StringBuilderExtensions.cs index a8d656c..2903265 100644 --- a/touki/Touki/Text/StringBuilderExtensions.cs +++ b/touki/Touki/Text/StringBuilderExtensions.cs @@ -2,8 +2,6 @@ // SPDX-License-Identifier: MIT // See LICENSE file in the project root for full license information -using System.Text; - namespace Touki.Text; /// diff --git a/touki/Touki/Text/StringSegment.cs b/touki/Touki/Text/StringSegment.cs index a85ba79..3ea46fd 100644 --- a/touki/Touki/Text/StringSegment.cs +++ b/touki/Touki/Text/StringSegment.cs @@ -7,7 +7,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; using System.Globalization; namespace Touki.Text; @@ -997,4 +996,25 @@ private static unsafe bool CompareOrdinalIgnoreCaseAscii(char* a, int lengthA, c /// The string to compare with. /// if the segment is greater than or equal to the string; otherwise, . public static bool operator >=(StringSegment left, string right) => left.CompareTo(right) >= 0; + + /// + /// Writes the segment to the specified . + /// + public void WriteTo(TextWriter writer) + { + ArgumentNullException.ThrowIfNull(writer); + + if (_length == 0) + { + return; + } + + if (_value?.Length == _length) + { + writer.Write(_value); + return; + } + + writer.Write(AsSpan()); + } } diff --git a/touki/Touki/Text/ValueStringBuilder.cs b/touki/Touki/Text/ValueStringBuilder.cs index b55b719..d89df49 100644 --- a/touki/Touki/Text/ValueStringBuilder.cs +++ b/touki/Touki/Text/ValueStringBuilder.cs @@ -7,8 +7,12 @@ // Original source: .NET Runtime and Windows Forms source code. -using System.Buffers; +#if NETFRAMEWORK +using System.CodeDom.Compiler; +#endif + using System.Globalization; +using Touki.Io; namespace Touki.Text; @@ -516,14 +520,14 @@ private void GrowAndAppend(char c) } /// - /// Resize the internal buffer either by doubling current buffer size or - /// by adding to - /// whichever is greater. + /// Resize the internal buffer either by doubling current buffer size or by adding + /// to whichever is greater. /// /// /// Number of chars requested beyond current position. /// [MethodImpl(MethodImplOptions.NoInlining)] + [MemberNotNull(nameof(_arrayToReturnToPool))] private void Grow(int additionalCapacityBeyondPos) { Debug.Assert(additionalCapacityBeyondPos > 0); @@ -618,10 +622,17 @@ public void CopyTo(Stream stream) } /// - /// Writes the string to the specified . + /// Writes the string to the specified . /// - public void CopyTo(T writer) where T : System.IO.StreamWriter + /// + /// + /// This attempts to make use of optimized paths for known types. + /// + /// + public void CopyTo(TextWriter writer) { + ArgumentNullException.ThrowIfNull(writer); + if (_chars[.._length].IsEmpty) { return; @@ -630,15 +641,32 @@ public void CopyTo(T writer) where T : System.IO.StreamWriter #if NET writer.Write(AsSpan()); #else - if (typeof(T) != typeof(System.IO.StreamWriter)) + if (writer is StringWriter stringWriter) { - throw new InvalidOperationException("Derived classes are not supported for safety."); + StringBuilder builder = stringWriter.GetStringBuilder(); + builder.AppendSpan(AsSpan()); + return; + } + + if (writer is IndentedTextWriter indentedWriter) + { + // Get the writer to write out tabs if needed and hope that the nested writer has an optimized path. + indentedWriter.Write(""); + CopyTo(indentedWriter.InnerWriter); + return; + } + + if (writer is not StreamWriter streamWriter || writer.GetType() != typeof(StreamWriter)) + { + // Have to call the extension, which will rent a char[] buffer. + writer.Write(AsSpan()); + return; } if (_arrayToReturnToPool is null) { // If we don't have a rented array, we need to rent one. - EnsureCapacity(_chars.Length + 1); + Grow(_chars.Length - _length + 1); } // More details are above with _arrayToReturnToPool. This is a dangerous cast as the Length will be twice as @@ -648,7 +676,7 @@ public void CopyTo(T writer) where T : System.IO.StreamWriter // Also not a terribly crazy idea to port the .NET implementation of StreamWriter, trim it down and seal it. - char[] chars = Unsafe.As(ref _arrayToReturnToPool!); + char[] chars = Unsafe.As(ref _arrayToReturnToPool); writer.Write(chars, 0, _length); #endif }