From 1fdc15c136f7fc649173e0902c4339ebe72d5010 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Mon, 12 Jun 2017 19:37:44 +0100 Subject: [PATCH] When adding spaces in place of tabs, calculate from start of line Maintain the length of the last line in AttributedStringBuilder so that future appends also calculate tab size correctly. If there are line breaks in a string, a tab character should add spaces up until the next tabstop from the beginning of the line not from the start of the string. --- .../jline/utils/AttributedStringBuilder.java | 32 +++++- .../utils/AttributedStringBuilderTest.java | 105 ++++++++++++++++++ 2 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 terminal/src/test/java/org/jline/utils/AttributedStringBuilderTest.java diff --git a/terminal/src/main/java/org/jline/utils/AttributedStringBuilder.java b/terminal/src/main/java/org/jline/utils/AttributedStringBuilder.java index a91d9604a..670d468cc 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) { @@ -149,6 +150,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++; } } @@ -321,6 +327,11 @@ public AttributedStringBuilder appendAnsi(String ansi) { ensureCapacity(length + 1); buffer[length] = c; style[length] = this.current.getStyle(); + if (c == '\n') { + lastLineLength = 0; + } else { + lastLineLength++; + } length++; } } @@ -328,21 +339,36 @@ public AttributedStringBuilder appendAnsi(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); + } +}