diff --git a/terminal/src/main/java/org/jline/utils/AttributedStringBuilder.java b/terminal/src/main/java/org/jline/utils/AttributedStringBuilder.java index 9f41892bb..e7bb11d48 100644 --- a/terminal/src/main/java/org/jline/utils/AttributedStringBuilder.java +++ b/terminal/src/main/java/org/jline/utils/AttributedStringBuilder.java @@ -26,6 +26,7 @@ public class AttributedStringBuilder extends AttributedCharSequence implements A private int[] style; private int length; private int tabs = 0; + private int lastLineLength = 0; private AttributedStyle current = AttributedStyle.DEFAULT; public static AttributedString append(CharSequence... strings) { @@ -157,6 +158,11 @@ public AttributedStringBuilder append(AttributedCharSequence str, int start, int ensureCapacity(length + 1); buffer[length] = c; style[length] = s; + if (c == '\n') { + lastLineLength = 0; + } else { + lastLineLength++; + } length++; } } @@ -333,6 +339,11 @@ public AttributedStringBuilder ansiAppend(String ansi) { ensureCapacity(length + 1); buffer[length] = c; style[length] = this.current.getStyle(); + if (c == '\n') { + lastLineLength = 0; + } else { + lastLineLength++; + } length++; } } @@ -340,21 +351,36 @@ public AttributedStringBuilder ansiAppend(String ansi) { } protected void insertTab(AttributedStyle s) { - int nb = tabs - length % tabs; + int nb = tabs - lastLineLength % tabs; ensureCapacity(length + nb); for (int i = 0; i < nb; i++) { buffer[length] = ' '; style[length] = s.getStyle(); length++; } + lastLineLength += nb; } public void setLength(int l) { length = l; } - public AttributedStringBuilder tabs(int tabs) { - this.tabs = tabs; + /** + * Set the number of spaces a tab is expanded to. Tab size cannot be changed + * after text has been added to prevent inconsistent indentation. + * + * If tab size is set to 0, tabs are not expanded (the default). + * @param tabsize Spaces per tab or 0 for no tab expansion. Must be non-negative + * @return + */ + public AttributedStringBuilder tabs(int tabsize) { + if (length > 0) { + throw new IllegalStateException("Cannot change tab size after appending text"); + } + if (tabsize < 0) { + throw new IllegalArgumentException("Tab size must be non negative"); + } + this.tabs = tabsize; return this; } diff --git a/terminal/src/test/java/org/jline/utils/AttributedStringBuilderTest.java b/terminal/src/test/java/org/jline/utils/AttributedStringBuilderTest.java new file mode 100644 index 000000000..0d137f7e6 --- /dev/null +++ b/terminal/src/test/java/org/jline/utils/AttributedStringBuilderTest.java @@ -0,0 +1,105 @@ +package org.jline.utils; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class AttributedStringBuilderTest { + private static String TAB_SIZE_ERR_MSG = "Incorrect tab size"; + + /** + * Test single line with tabs in + */ + @Test + public void testTabSize() { + AttributedStringBuilder sb; + sb = new AttributedStringBuilder().tabs(4); + sb.append("hello\tWorld"); + assertEquals(TAB_SIZE_ERR_MSG, "hello World", sb.toString()); + + sb = new AttributedStringBuilder().tabs(5); + sb.append("hello\tWorld"); + assertEquals(TAB_SIZE_ERR_MSG, "hello World", sb.toString()); + } + + /** + * Test multiple lines with tabs in + */ + @Test + public void testSplitLineTabSize() { + AttributedStringBuilder sb; + sb = new AttributedStringBuilder().tabs(4); + sb.append("hello\n\tWorld"); + assertEquals(TAB_SIZE_ERR_MSG, "hello\n World", sb.toString()); + + sb = new AttributedStringBuilder().tabs(4); + sb.append("hello\tWorld\n\tfoo\tbar"); + assertEquals(TAB_SIZE_ERR_MSG, "hello World\n foo bar", sb.toString()); + + sb = new AttributedStringBuilder().tabs(5); + sb.append("hello\n\tWorld"); + assertEquals(TAB_SIZE_ERR_MSG, "hello\n World", sb.toString()); + + sb = new AttributedStringBuilder().tabs(5); + sb.append("hello\tWorld\n\tfoo\tbar"); + assertEquals(TAB_SIZE_ERR_MSG, "hello World\n foo bar", sb.toString()); + } + + @Test + public void testAppendToString() { + AttributedStringBuilder sb; + String expected = ""; + sb = new AttributedStringBuilder().tabs(4); + + sb.append("hello"); expected += "hello"; + sb.append("\tWorld"); expected += " World"; //append to first line + assertEquals(TAB_SIZE_ERR_MSG, expected, sb.toString()); + + sb.append("\nfoo\tbar"); expected += "\nfoo bar"; //append new line + assertEquals(TAB_SIZE_ERR_MSG, expected, sb.toString()); + + sb.append("lorem\tipsum"); expected += "lorem ipsum"; //append to second line + assertEquals(TAB_SIZE_ERR_MSG, expected, sb.toString()); + } + + @Test + public void testFromAnsiWithTabs() { + AttributedStringBuilder sb; + String expected = ""; + sb = new AttributedStringBuilder().tabs(4); + + sb.appendAnsi("hello\tWorld"); expected += "hello World"; + assertEquals(TAB_SIZE_ERR_MSG, expected, sb.toString()); + + sb.appendAnsi("\033[38;5;120mgreen\tfoo\033[39m"); expected += "green foo"; + assertEquals(TAB_SIZE_ERR_MSG, expected, sb.toString()); + sb.appendAnsi("\n\033[38;5;120mbar\tbaz\033[39m"); expected += "\nbar baz"; + assertEquals(TAB_SIZE_ERR_MSG, expected, sb.toString()); + } + + /** + * Test that tabs are not expanded in strings if tab size has not been set + */ + @Test + public void testUnsetTabSize() { + AttributedStringBuilder sb; + String expected = ""; + sb = new AttributedStringBuilder(); + + sb.append("hello\tWorld"); expected += "hello\tWorld"; + assertEquals(TAB_SIZE_ERR_MSG, expected, sb.toString()); + } + + @Test(expected=IllegalStateException.class) + public void testChangingExistingTabSize() throws Exception { + AttributedStringBuilder sb = new AttributedStringBuilder(); + sb.append("helloWorld"); + sb.tabs(4); + } + + @Test(expected=IllegalArgumentException.class) + public void testNegativeTabSize() throws Exception { + @SuppressWarnings("unused") + AttributedStringBuilder sb = new AttributedStringBuilder().tabs(-1); + } +}