mirror of
https://github.com/Ellpeck/MLEM.git
synced 2024-11-24 21:48:35 +01:00
Compare commits
No commits in common. "9aef994c510ba0c59845442f5210cfe43842945f" and "fd5b83eaa0c9ec8824d38e632d96dbd74c9ff7e4" have entirely different histories.
9aef994c51
...
fd5b83eaa0
6 changed files with 50 additions and 185 deletions
|
@ -10,13 +10,8 @@ Jump to version:
|
|||
### MLEM
|
||||
Additions
|
||||
- Added a strikethrough formatting code
|
||||
- Added GenericFont SplitStringSeparate which differentiates between existing newline characters and splits due to maximum width
|
||||
|
||||
### MLEM.Ui
|
||||
Additions
|
||||
- Allow specifying a maximum amount of characters for a TextField
|
||||
- Added a multiline editing mode to TextField
|
||||
|
||||
Improvements
|
||||
- Cache TokenizedString inner offsets for non-Left text alignments to improve performance
|
||||
|
||||
|
|
|
@ -95,8 +95,8 @@ namespace Demos {
|
|||
this.root.AddChild(new Paragraph(Anchor.AutoLeft, 1, "Defining text animations as formatting codes is also possible, including <a wobbly>wobbly text</a> at <a wobbly 8 0.25>different intensities</a>. Of course, more animations can be added though."));
|
||||
|
||||
this.root.AddChild(new VerticalSpace(3));
|
||||
this.root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Multiline text input:", true));
|
||||
this.root.AddChild(new TextField(Anchor.AutoLeft, new Vector2(1, 50), multiline: true) {
|
||||
this.root.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Text input:", true));
|
||||
this.root.AddChild(new TextField(Anchor.AutoLeft, new Vector2(1, 10)) {
|
||||
PositionOffset = new Vector2(0, 1),
|
||||
PlaceholderText = "Click here to input text"
|
||||
});
|
||||
|
|
|
@ -25,7 +25,7 @@ namespace MLEM.Ui.Elements {
|
|||
/// </summary>
|
||||
public static readonly Rule DefaultRule = (field, add) => {
|
||||
foreach (var c in add) {
|
||||
if (char.IsControl(c) && (!field.Multiline || c != '\n'))
|
||||
if (char.IsControl(c))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
@ -135,12 +135,13 @@ namespace MLEM.Ui.Elements {
|
|||
/// This is always between 0 and the <see cref="string.Length"/> of <see cref="Text"/>
|
||||
/// </summary>
|
||||
public int CaretPos {
|
||||
get => this.caretPos;
|
||||
get {
|
||||
this.CaretPos = MathHelper.Clamp(this.caretPos, 0, this.text.Length);
|
||||
return this.caretPos;
|
||||
}
|
||||
set {
|
||||
var val = MathHelper.Clamp(value, 0, this.text.Length);
|
||||
if (this.caretPos != val) {
|
||||
this.caretPos = val;
|
||||
this.caretBlinkTimer = 0;
|
||||
if (this.caretPos != value) {
|
||||
this.caretPos = value;
|
||||
this.HandleTextChange(false);
|
||||
}
|
||||
}
|
||||
|
@ -157,23 +158,6 @@ namespace MLEM.Ui.Elements {
|
|||
this.HandleTextChange(false);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// The maximum amount of characters that can be input into this text field.
|
||||
/// If this is set, the length of <see cref="Text"/> will never exceed this value.
|
||||
/// </summary>
|
||||
public int? MaximumCharacters;
|
||||
/// <summary>
|
||||
/// Whether this text field should support multi-line editing.
|
||||
/// If this is true, pressing <see cref="Keys.Enter"/> will insert a new line into the <see cref="Text"/> if the <see cref="InputRule"/> allows it.
|
||||
/// Additionally, text will be rendered with horizontal soft wraps, and lines that are outside of the text field's bounds will be hidden.
|
||||
/// </summary>
|
||||
public bool Multiline {
|
||||
get => this.multiline;
|
||||
set {
|
||||
this.multiline = value;
|
||||
this.HandleTextChange(false);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly StringBuilder text = new StringBuilder();
|
||||
|
||||
|
@ -181,9 +165,7 @@ namespace MLEM.Ui.Elements {
|
|||
private double caretBlinkTimer;
|
||||
private string displayedText;
|
||||
private int textOffset;
|
||||
private int lineOffset;
|
||||
private int caretPos;
|
||||
private bool multiline;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new text field with the given settings
|
||||
|
@ -193,10 +175,8 @@ namespace MLEM.Ui.Elements {
|
|||
/// <param name="rule">The text field's input rule</param>
|
||||
/// <param name="font">The font to use for drawing text</param>
|
||||
/// <param name="text">The text that the text field should contain by default</param>
|
||||
/// <param name="multiline">Whether the text field should support multi-line editing</param>
|
||||
public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null, string text = null, bool multiline = false) : base(anchor, size) {
|
||||
public TextField(Anchor anchor, Vector2 size, Rule rule = null, GenericFont font = null, string text = null) : base(anchor, size) {
|
||||
this.InputRule = rule ?? DefaultRule;
|
||||
this.Multiline = multiline;
|
||||
if (font != null)
|
||||
this.Font.Set(font);
|
||||
if (text != null)
|
||||
|
@ -215,8 +195,6 @@ namespace MLEM.Ui.Elements {
|
|||
}
|
||||
} else if (key == Keys.Delete) {
|
||||
this.RemoveText(this.CaretPos, 1);
|
||||
} else if (this.Multiline && key == Keys.Enter) {
|
||||
this.InsertText('\n');
|
||||
} else {
|
||||
this.InsertText(character);
|
||||
}
|
||||
|
@ -228,7 +206,7 @@ namespace MLEM.Ui.Elements {
|
|||
var title = this.MobileTitle ?? this.PlaceholderText;
|
||||
var result = await MlemPlatform.Current.OpenOnScreenKeyboard(title, this.MobileDescription, this.Text, false);
|
||||
if (result != null)
|
||||
this.SetText(this.Multiline ? result : result.Replace('\n', ' '), true);
|
||||
this.SetText(result.Replace('\n', ' '), true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,83 +214,26 @@ namespace MLEM.Ui.Elements {
|
|||
// not initialized yet
|
||||
if (!this.Font.HasValue())
|
||||
return;
|
||||
var length = this.Font.Value.MeasureString(this.text.ToString()).X * this.TextScale;
|
||||
var maxWidth = this.DisplayArea.Width / this.Scale - this.TextOffsetX * 2;
|
||||
if (this.Multiline) {
|
||||
// soft wrap if we're multiline
|
||||
this.displayedText = this.Font.Value.SplitString(this.text.ToString(), maxWidth, this.TextScale);
|
||||
var maxHeight = this.DisplayArea.Height / this.Scale - this.TextOffsetX * 2;
|
||||
if (this.Font.Value.MeasureString(this.displayedText).Y * this.TextScale > maxHeight) {
|
||||
var maxLines = (maxHeight / (this.Font.Value.LineHeight * this.TextScale)).Floor();
|
||||
// calculate what line the caret is on
|
||||
var caretLine = 0;
|
||||
var originalIndex = 0;
|
||||
var addedLineBreaks = 0;
|
||||
for (var i = 0; i <= this.CaretPos + addedLineBreaks - 1 && i < this.displayedText.Length; i++) {
|
||||
if (this.displayedText[i] == '\n') {
|
||||
caretLine++;
|
||||
if (this.text[originalIndex] != '\n') {
|
||||
addedLineBreaks++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
originalIndex++;
|
||||
}
|
||||
// when we're multiline, the text offset is measured in lines
|
||||
if (this.lineOffset > caretLine) {
|
||||
// if we're moving up
|
||||
this.lineOffset = caretLine;
|
||||
} else if (caretLine >= maxLines) {
|
||||
// if we're moving down
|
||||
var limit = caretLine - (maxLines - 1);
|
||||
if (limit > this.lineOffset)
|
||||
this.lineOffset = limit;
|
||||
}
|
||||
// calculate resulting string
|
||||
var ret = new StringBuilder();
|
||||
var lines = 0;
|
||||
originalIndex = 0;
|
||||
for (var i = 0; i < this.displayedText.Length; i++) {
|
||||
if (lines >= this.lineOffset) {
|
||||
if (ret.Length <= 0)
|
||||
this.textOffset = originalIndex;
|
||||
ret.Append(this.displayedText[i]);
|
||||
}
|
||||
if (this.displayedText[i] == '\n') {
|
||||
lines++;
|
||||
if (this.text[originalIndex] == '\n')
|
||||
originalIndex++;
|
||||
} else {
|
||||
originalIndex++;
|
||||
}
|
||||
if (lines - this.lineOffset >= maxLines)
|
||||
break;
|
||||
}
|
||||
this.displayedText = ret.ToString();
|
||||
} else {
|
||||
this.lineOffset = 0;
|
||||
this.textOffset = 0;
|
||||
}
|
||||
} else {
|
||||
// not multiline, so scroll horizontally based on caret position
|
||||
if (this.Font.Value.MeasureString(this.text.ToString()).X * this.TextScale > maxWidth) {
|
||||
if (this.textOffset > this.CaretPos) {
|
||||
if (length > maxWidth) {
|
||||
// if we're moving the caret to the left
|
||||
if (this.textOffset > this.CaretPos) {
|
||||
this.textOffset = this.CaretPos;
|
||||
} else {
|
||||
// if we're moving the caret to the right
|
||||
var importantArea = this.text.ToString(this.textOffset, Math.Min(this.CaretPos, this.text.Length) - this.textOffset);
|
||||
var bound = this.CaretPos - this.Font.Value.TruncateString(importantArea, maxWidth, this.TextScale, true).Length;
|
||||
if (this.textOffset < bound)
|
||||
if (this.textOffset < bound) {
|
||||
this.textOffset = bound;
|
||||
}
|
||||
}
|
||||
var visible = this.text.ToString(this.textOffset, this.text.Length - this.textOffset);
|
||||
this.displayedText = this.Font.Value.TruncateString(visible, maxWidth, this.TextScale);
|
||||
} else {
|
||||
this.displayedText = this.Text;
|
||||
this.textOffset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.MaskingCharacter != null)
|
||||
this.displayedText = new string(this.MaskingCharacter.Value, this.displayedText.Length);
|
||||
|
||||
|
@ -367,38 +288,13 @@ namespace MLEM.Ui.Elements {
|
|||
|
||||
if (this.displayedText != null) {
|
||||
var lineHeight = this.Font.Value.LineHeight * this.TextScale * this.Scale;
|
||||
var offset = new Vector2(
|
||||
this.TextOffsetX * this.Scale,
|
||||
this.Multiline ? this.TextOffsetX * this.Scale : this.DisplayArea.Height / 2 - lineHeight / 2);
|
||||
var textPos = this.DisplayArea.Location + offset;
|
||||
var textPos = this.DisplayArea.Location + new Vector2(this.TextOffsetX * this.Scale, this.DisplayArea.Height / 2 - lineHeight / 2);
|
||||
if (this.text.Length > 0 || this.IsSelected) {
|
||||
var textColor = this.TextColor.OrDefault(Color.White);
|
||||
this.Font.Value.DrawString(batch, this.displayedText, textPos, textColor * alpha, 0, Vector2.Zero, this.TextScale * this.Scale, SpriteEffects.None, 0);
|
||||
|
||||
if (this.IsSelected && this.caretBlinkTimer < 0.5F) {
|
||||
var caretDrawPos = textPos;
|
||||
if (this.Multiline) {
|
||||
var lines = 0;
|
||||
var lastLineBreak = 0;
|
||||
var originalIndex = 0;
|
||||
var addedLineBreaks = 0;
|
||||
for (var i = 0; i <= this.CaretPos - this.textOffset + addedLineBreaks - 1 && i < this.displayedText.Length; i++) {
|
||||
if (this.displayedText[i] == '\n') {
|
||||
lines++;
|
||||
lastLineBreak = i;
|
||||
if (this.text[originalIndex] != '\n') {
|
||||
addedLineBreaks++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
originalIndex++;
|
||||
}
|
||||
var sub = this.displayedText.Substring(lastLineBreak, this.CaretPos - this.textOffset + addedLineBreaks - lastLineBreak);
|
||||
caretDrawPos += new Vector2(this.Font.Value.MeasureString(sub).X * this.TextScale * this.Scale, lineHeight * lines);
|
||||
} else {
|
||||
caretDrawPos.X += this.Font.Value.MeasureString(this.displayedText.Substring(0, this.CaretPos - this.textOffset)).X * this.TextScale * this.Scale;
|
||||
}
|
||||
batch.Draw(batch.GetBlankTexture(), new RectangleF(caretDrawPos, new Vector2(this.CaretWidth * this.Scale, lineHeight)), null, textColor * alpha);
|
||||
if (this.IsSelected && this.caretBlinkTimer >= 0.5F) {
|
||||
var textSize = this.Font.Value.MeasureString(this.displayedText.Substring(0, this.CaretPos - this.textOffset)) * this.TextScale * this.Scale;
|
||||
batch.Draw(batch.GetBlankTexture(), new RectangleF(textPos.X + textSize.X, textPos.Y, this.CaretWidth * this.Scale, lineHeight), null, textColor * alpha);
|
||||
}
|
||||
} else if (this.PlaceholderText != null) {
|
||||
this.Font.Value.DrawString(batch, this.PlaceholderText, textPos, this.PlaceholderColor.OrDefault(Color.Gray) * alpha, 0, Vector2.Zero, this.TextScale * this.Scale, SpriteEffects.None, 0);
|
||||
|
@ -409,40 +305,33 @@ namespace MLEM.Ui.Elements {
|
|||
|
||||
/// <summary>
|
||||
/// Replaces this text field's text with the given text.
|
||||
/// If the resulting <see cref="Text"/> exceeds <see cref="MaximumCharacters"/>, the end will be cropped to fit.
|
||||
/// </summary>
|
||||
/// <param name="text">The new text</param>
|
||||
/// <param name="removeMismatching">If any characters that don't match the <see cref="InputRule"/> should be left out</param>
|
||||
public void SetText(object text, bool removeMismatching = false) {
|
||||
var strg = text?.ToString() ?? string.Empty;
|
||||
if (removeMismatching) {
|
||||
var result = new StringBuilder();
|
||||
foreach (var c in strg) {
|
||||
foreach (var c in text.ToString()) {
|
||||
if (this.InputRule(this, c.ToCachedString()))
|
||||
result.Append(c);
|
||||
}
|
||||
strg = result.ToString();
|
||||
} else if (!this.InputRule(this, strg))
|
||||
text = result.ToString();
|
||||
} else if (!this.InputRule(this, text.ToString()))
|
||||
return;
|
||||
if (this.MaximumCharacters != null && strg.Length > this.MaximumCharacters)
|
||||
strg = strg.Substring(0, this.MaximumCharacters.Value);
|
||||
this.text.Clear();
|
||||
this.text.Append(strg);
|
||||
this.text.Append(text);
|
||||
this.CaretPos = this.text.Length;
|
||||
this.HandleTextChange();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the given text at the <see cref="CaretPos"/>.
|
||||
/// If the resulting <see cref="Text"/> exceeds <see cref="MaximumCharacters"/>, the end will be cropped to fit.
|
||||
/// Inserts the given text at the <see cref="CaretPos"/>
|
||||
/// </summary>
|
||||
/// <param name="text">The text to insert</param>
|
||||
public void InsertText(object text) {
|
||||
var strg = text.ToString();
|
||||
if (!this.InputRule(this, strg))
|
||||
return;
|
||||
if (this.MaximumCharacters != null && this.text.Length + strg.Length > this.MaximumCharacters)
|
||||
strg = strg.Substring(0, this.MaximumCharacters.Value - this.text.Length);
|
||||
this.text.Insert(this.CaretPos, strg);
|
||||
this.CaretPos += strg.Length;
|
||||
this.HandleTextChange();
|
||||
|
@ -457,8 +346,6 @@ namespace MLEM.Ui.Elements {
|
|||
if (index < 0 || index >= this.text.Length)
|
||||
return;
|
||||
this.text.Remove(index, length);
|
||||
// ensure that caret pos is still in bounds
|
||||
this.CaretPos = this.CaretPos;
|
||||
this.HandleTextChange();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
|
@ -161,36 +160,22 @@ namespace MLEM.Font {
|
|||
|
||||
/// <summary>
|
||||
/// Splits a string to a given maximum width, adding newline characters between each line.
|
||||
/// Also splits long words and supports zero-width spaces and takes into account existing newline characters in the passed <paramref name="text"/>.
|
||||
/// See <see cref="SplitStringSeparate"/> for a method that differentiates between existing newline characters and splits due to maximum width.
|
||||
/// Also splits long words and supports zero-width spaces.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to split into multiple lines</param>
|
||||
/// <param name="width">The maximum width that each line should have</param>
|
||||
/// <param name="scale">The scale to use for width measurements</param>
|
||||
/// <returns>The split string, containing newline characters at each new line</returns>
|
||||
public string SplitString(string text, float width, float scale) {
|
||||
return string.Join("\n", this.SplitStringSeparate(text, width, scale));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a string to a given maximum width and returns each split section as a separate string.
|
||||
/// Note that existing new lines are taken into account for line length, but not split in the resulting strings.
|
||||
/// This method differs from <see cref="SplitString"/> in that it differentiates between pre-existing newline characters and splits due to maximum width.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to split into multiple lines</param>
|
||||
/// <param name="width">The maximum width that each line should have</param>
|
||||
/// <param name="scale">The scale to use for width measurements</param>
|
||||
/// <returns>The split string as an enumerable of split sections</returns>
|
||||
public IEnumerable<string> SplitStringSeparate(string text, float width, float scale) {
|
||||
var ret = new StringBuilder();
|
||||
var currWidth = 0F;
|
||||
var lastSpaceIndex = -1;
|
||||
var widthSinceLastSpace = 0F;
|
||||
var curr = new StringBuilder();
|
||||
for (var i = 0; i < text.Length; i++) {
|
||||
var c = text[i];
|
||||
if (c == '\n') {
|
||||
// fake split at pre-defined new lines
|
||||
curr.Append(c);
|
||||
// split at pre-defined new lines
|
||||
ret.Append(c);
|
||||
lastSpaceIndex = -1;
|
||||
widthSinceLastSpace = 0;
|
||||
currWidth = 0;
|
||||
|
@ -198,19 +183,17 @@ namespace MLEM.Font {
|
|||
var cWidth = this.MeasureString(c.ToCachedString()).X * scale;
|
||||
if (c == ' ' || c == OneEmSpace || c == Zwsp) {
|
||||
// remember the location of this (breaking!) space
|
||||
lastSpaceIndex = curr.Length;
|
||||
lastSpaceIndex = ret.Length;
|
||||
widthSinceLastSpace = 0;
|
||||
} else if (currWidth + cWidth >= width) {
|
||||
// check if this line contains a space
|
||||
if (lastSpaceIndex < 0) {
|
||||
// if there is no last space, the word is longer than a line so we split here
|
||||
yield return curr.ToString();
|
||||
ret.Append('\n');
|
||||
currWidth = 0;
|
||||
curr.Clear();
|
||||
} else {
|
||||
// split after the last space
|
||||
yield return curr.ToString().Substring(0, lastSpaceIndex + 1);
|
||||
curr.Remove(0, lastSpaceIndex + 1);
|
||||
ret.Insert(lastSpaceIndex + 1, '\n');
|
||||
// we need to restore the width accumulated since the last space for the new line
|
||||
currWidth = widthSinceLastSpace;
|
||||
}
|
||||
|
@ -221,11 +204,10 @@ namespace MLEM.Font {
|
|||
// add current character
|
||||
currWidth += cWidth;
|
||||
widthSinceLastSpace += cWidth;
|
||||
curr.Append(c);
|
||||
ret.Append(c);
|
||||
}
|
||||
}
|
||||
if (curr.Length > 0)
|
||||
yield return curr.ToString();
|
||||
return ret.ToString();
|
||||
}
|
||||
|
||||
private static bool IsTrailingSpace(string s, int index) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
|
|
@ -29,9 +29,9 @@ namespace Tests {
|
|||
|
||||
[Test]
|
||||
public void TestRegularSplit() {
|
||||
Assert.AreEqual(this.font.SplitStringSeparate(
|
||||
Assert.AreEqual(this.font.SplitString(
|
||||
"Note that the default style does not contain any textures or font files and, as such, is quite bland. However, the default style is quite easy to override, as can be seen in the code for this demo.",
|
||||
65, 0.1F), new[] {"Note that the default style does ", "not contain any textures or font ", "files and, as such, is quite ", "bland. However, the default ", "style is quite easy to override, ", "as can be seen in the code for ", "this demo."});
|
||||
65, 0.1F), "Note that the default style does \nnot contain any textures or font \nfiles and, as such, is quite \nbland. However, the default \nstyle is quite easy to override, \nas can be seen in the code for \nthis demo.");
|
||||
|
||||
var formatted = this.formatter.Tokenize(this.font,
|
||||
"Select the demo you want to see below using your mouse, touch input, your keyboard or a controller. Check the demos' <c CornflowerBlue><l https://github.com/Ellpeck/MLEM/tree/main/Demos>source code</l></c> for more in-depth explanations of their functionality or the <c CornflowerBlue><l https://mlem.ellpeck.de/>website</l></c> for tutorials and API documentation.");
|
||||
|
@ -55,16 +55,16 @@ namespace Tests {
|
|||
|
||||
[Test]
|
||||
public void TestLongLineSplit() {
|
||||
var expectedDisplay = new[] {"This_is_a_really_long_line_to_s", "ee_if_splitting_without_spaces_", "works_properly._I_also_want_to_", "see_if_it_works_across_multiple", "_lines_or_just_on_the_first_one. ", "But after this, I want the text to ", "continue normally before ", "changing_back_to_being_really_", "long_oh_yes"};
|
||||
const string expectedDisplay = "This_is_a_really_long_line_to_s\nee_if_splitting_without_spaces_\nworks_properly._I_also_want_to_\nsee_if_it_works_across_multiple\n_lines_or_just_on_the_first_one. \nBut after this, I want the text to \ncontinue normally before \nchanging_back_to_being_really_\nlong_oh_yes";
|
||||
|
||||
Assert.AreEqual(this.font.SplitStringSeparate(
|
||||
Assert.AreEqual(this.font.SplitString(
|
||||
"This_is_a_really_long_line_to_see_if_splitting_without_spaces_works_properly._I_also_want_to_see_if_it_works_across_multiple_lines_or_just_on_the_first_one. But after this, I want the text to continue normally before changing_back_to_being_really_long_oh_yes",
|
||||
65, 0.1F), expectedDisplay);
|
||||
|
||||
var formatted = this.formatter.Tokenize(this.font,
|
||||
"This_is_a_really_long_line_to_see_if_<c Blue>splitting</c>_without_spaces_works_properly._I_also_want_to_see_if_it_works_across_multiple_<c Yellow>lines</c>_or_just_on_the_first_one. But after this, I want the <b>text</b> to continue normally before changing_back_<i>to</i>_being_really_long_oh_yes");
|
||||
formatted.Split(this.font, 65, 0.1F);
|
||||
Assert.AreEqual(formatted.DisplayString, string.Join('\n', expectedDisplay));
|
||||
Assert.AreEqual(formatted.DisplayString, expectedDisplay);
|
||||
|
||||
var tokens = new[] {
|
||||
"This_is_a_really_long_line_to_s\nee_if_",
|
||||
|
|
Loading…
Reference in a new issue