diff --git a/src/terminal/ControlSequenceHandler.java b/src/terminal/ControlSequenceHandler.java index bc89a5d..341d7de 100644 --- a/src/terminal/ControlSequenceHandler.java +++ b/src/terminal/ControlSequenceHandler.java @@ -3,11 +3,11 @@ package terminal; import static terminal.ControlSequenceHandler.Command.SGR; class ControlSequenceHandler { - + public static final boolean isEscape(char c) { return c == '\u001B'; } - + private boolean inControlSequence; private int code; private Command command; @@ -18,7 +18,7 @@ class ControlSequenceHandler { this.code = 0; this.command = SGR; } - + public static enum Command { SGR } diff --git a/src/terminal/LispTerminal.java b/src/terminal/LispTerminal.java index a952254..38c9289 100644 --- a/src/terminal/LispTerminal.java +++ b/src/terminal/LispTerminal.java @@ -6,21 +6,24 @@ import static util.Characters.EOF; import java.io.*; import java.util.concurrent.*; -import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.*; import com.googlecode.lanterna.input.*; -import com.googlecode.lanterna.terminal.IOSafeTerminal; +import com.googlecode.lanterna.terminal.*; + +import terminal.SafePipedStream.*; public class LispTerminal { public static final char END_OF_SEGMENT = 'x'; private IOSafeTerminal terminal; - private PipedOutputStream inputWriter; - private PipedInputStream outputReader; + private SafePipedOutputStream inputWriter; + private SafePipedInputStream outputReader; private ExecutorService executorService; private ControlSequenceHandler controlSequenceHandler; - private TerminalPosition origin; - private TerminalPosition leadingEdge; + private TerminalSize terminalSize; + private int originColumn; + private int originRow; private String inputLine; private String outputSegment; private boolean isFinished; @@ -28,15 +31,32 @@ public class LispTerminal { public LispTerminal(IOSafeTerminal terminal, PipedOutputStream inputWriter, PipedInputStream outputReader) { // FIXME - add resize handler this.terminal = terminal; - this.inputWriter = inputWriter; - this.outputReader = outputReader; + this.inputWriter = new SafePipedOutputStream(inputWriter); + this.outputReader = new SafePipedInputStream(outputReader); this.executorService = Executors.newFixedThreadPool(2); this.controlSequenceHandler = new ControlSequenceHandler(); - this.origin = terminal.getCursorPosition(); - this.leadingEdge = origin; + this.terminalSize = terminal.getTerminalSize(); + this.originColumn = terminal.getCursorPosition().getColumn(); + this.originRow = terminal.getCursorPosition().getRow(); this.inputLine = ""; this.outputSegment = ""; this.isFinished = false; + + addResizeListener(); + } + + private void addResizeListener() { + terminal.addResizeListener(new TerminalResizeListener() { + + @Override + public void onResized(Terminal terminal, TerminalSize newSize) { + resize(); + } + }); + } + + private void resize() { + terminalSize = terminal.getTerminalSize(); } public void run() { @@ -51,8 +71,12 @@ public class LispTerminal { } private void processNextKey() { - handleKey(getKeyStroke()); - takeNap(); + KeyStroke keyStroke = getKeyStroke(); + + if (keyStroke != null) + handleKey(keyStroke); + else + takeNap(); } private KeyStroke getKeyStroke() { @@ -61,8 +85,9 @@ public class LispTerminal { try { keyStroke = terminal.pollInput(); } catch (IllegalStateException e) { - // TODO - Issue #299 + // Issue #299 terminal.putCharacter('\n'); + terminal.close(); System.exit(0); } @@ -70,9 +95,6 @@ public class LispTerminal { } private synchronized void handleKey(KeyStroke keyStroke) { - if (keyStroke == null) - return; - doKey(keyStroke); terminal.flush(); } @@ -121,22 +143,31 @@ public class LispTerminal { } private synchronized boolean isPossibleToMoveLeft(TerminalPosition cursorPosition) { - return distanceFromOrigin(cursorPosition) > 0; + return getDistanceFromOrigin(cursorPosition) > 0; } - private synchronized int distanceFromOrigin(TerminalPosition cursorPosition) { - int columnDifference = cursorPosition.getColumn() - origin.getColumn(); - int rowDifference = cursorPosition.getRow() - origin.getRow(); - int totalColumns = terminal.getTerminalSize().getColumns(); + private synchronized int getDistanceFromOrigin(TerminalPosition cursorPosition) { + int columnDifference = cursorPosition.getColumn() - originColumn; + int rowDifference = cursorPosition.getRow() - originRow; + int totalColumns = terminalSize.getColumns(); return columnDifference + (totalColumns * rowDifference); } + private synchronized TerminalPosition getLeadingEdge() { + int inputLength = inputLine.length(); + int totalColumns = terminalSize.getColumns(); + int rowDifference = inputLength / totalColumns; + int columnDifference = inputLength % totalColumns; + + return new TerminalPosition(originColumn + columnDifference, originRow + rowDifference); + } + private synchronized void retractCursor(TerminalPosition cursorPosition) { TerminalPosition newPosition = cursorPosition.withRelativeColumn(-1); if (isAtStartOfRow(cursorPosition)) - newPosition = cursorPosition.withColumn(terminal.getTerminalSize().getColumns()).withRelativeRow(-1); + newPosition = cursorPosition.withColumn(terminalSize.getColumns()).withRelativeRow(-1); terminal.setCursorPosition(newPosition); @@ -154,7 +185,7 @@ public class LispTerminal { } private synchronized boolean isPossibleToMoveRight(TerminalPosition cursorPosition) { - return distanceFromOrigin(cursorPosition) < inputLine.length(); + return getDistanceFromOrigin(cursorPosition) < inputLine.length(); } private synchronized void advanceCursor(TerminalPosition cursorPosition) { @@ -162,16 +193,10 @@ public class LispTerminal { moveCursorToNextRow(cursorPosition); else terminal.setCursorPosition(cursorPosition.withRelativeColumn(1)); - - TerminalPosition newPosition = terminal.getCursorPosition(); - - // FIXME - use distance from origin to set leading edge - if (distanceFromOrigin(newPosition) == inputLine.length()) - leadingEdge = terminal.getCursorPosition(); } private synchronized boolean isAtEndOfRow(TerminalPosition cursorPosition) { - return cursorPosition.getColumn() >= terminal.getTerminalSize().getColumns() - 1; + return cursorPosition.getColumn() >= terminalSize.getColumns() - 1; } private void moveCursorToNextRow(TerminalPosition cursorPosition) { @@ -182,40 +207,42 @@ public class LispTerminal { } private boolean isEndOfBuffer() { - return terminal.getCursorPosition().getRow() == terminal.getTerminalSize().getRows() - 1; + return terminal.getCursorPosition().getRow() == terminalSize.getRows() - 1; } private void createNewRowForCursor() { TerminalPosition originalPosition = terminal.getCursorPosition(); terminal.putCharacter('\n'); terminal.setCursorPosition(originalPosition.withColumn(0)); - origin = origin.withRelativeRow(-1); + --originRow; } private synchronized void doEnter() { moveCursorToEndOfInput(); terminal.putCharacter('\n'); inputLine += "\n"; - - try { - inputWriter.write(inputLine.getBytes()); - inputWriter.flush(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - + inputWriter.write(inputLine.getBytes()); + inputWriter.flush(); inputLine = ""; - origin = terminal.getCursorPosition(); - leadingEdge = origin; + updateOrigin(); + } + + private synchronized void moveCursorToEndOfInput() { + terminal.setCursorPosition(getLeadingEdge()); + } + + private void updateOrigin() { + TerminalPosition cursorPosition = terminal.getCursorPosition(); + originColumn = cursorPosition.getColumn(); + originRow = cursorPosition.getRow(); } private synchronized void doBackspace() { TerminalPosition cursorPosition = terminal.getCursorPosition(); if (isPossibleToMoveLeft(cursorPosition)) { - String remaining = inputLine.substring(distanceFromOrigin(cursorPosition), inputLine.length()); - inputLine = inputLine.substring(0, distanceFromOrigin(cursorPosition) - 1) + remaining; + String remaining = inputLine.substring(getDistanceFromOrigin(cursorPosition), inputLine.length()); + inputLine = inputLine.substring(0, getDistanceFromOrigin(cursorPosition) - 1) + remaining; retractCursor(cursorPosition); @@ -225,10 +252,6 @@ public class LispTerminal { terminal.putCharacter(' '); retractCursor(cursorPosition); - - // FIXME - use distance from origin to set leading edge - if (distanceFromOrigin(leadingEdge) > inputLine.length()) - leadingEdge = terminal.getCursorPosition(); } } @@ -236,18 +259,14 @@ public class LispTerminal { TerminalPosition cursorPosition = terminal.getCursorPosition(); if (isPossibleToMoveRight(cursorPosition)) { - String remaining = inputLine.substring(distanceFromOrigin(cursorPosition) + 1, inputLine.length()); - inputLine = inputLine.substring(0, distanceFromOrigin(cursorPosition)) + remaining; + String remaining = inputLine.substring(getDistanceFromOrigin(cursorPosition) + 1, inputLine.length()); + inputLine = inputLine.substring(0, getDistanceFromOrigin(cursorPosition)) + remaining; for (char c : remaining.toCharArray()) terminal.putCharacter(c); terminal.putCharacter(' '); terminal.setCursorPosition(cursorPosition); - - // FIXME - use distance from origin to set leading edge - if (distanceFromOrigin(leadingEdge) > inputLine.length()) - leadingEdge = terminal.getCursorPosition(); } } @@ -256,8 +275,8 @@ public class LispTerminal { if (isPossibleToMoveRight(cursorPosition)) { String remaining = keyStroke.getCharacter() - + inputLine.substring(distanceFromOrigin(cursorPosition), inputLine.length()); - inputLine = inputLine.substring(0, distanceFromOrigin(cursorPosition)) + remaining; + + inputLine.substring(getDistanceFromOrigin(cursorPosition), inputLine.length()); + inputLine = inputLine.substring(0, getDistanceFromOrigin(cursorPosition)) + remaining; // FIXME - must have a way to push remainder on to a new line at the end of the buffer @@ -282,13 +301,8 @@ public class LispTerminal { } public void writeOutput() { - try { - for (int c = outputReader.read(); c != EOF; c = outputReader.read()) - processOutput((char) c); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } + for (int c = outputReader.read(); c != EOF; c = outputReader.read()) + processOutput((char) c); terminal.setCursorVisible(true); terminal.flush(); @@ -321,12 +335,7 @@ public class LispTerminal { terminal.flush(); terminal.setCursorVisible(true); outputSegment = ""; - origin = terminal.getCursorPosition(); - leadingEdge = origin; - } - - private synchronized void moveCursorToEndOfInput() { - terminal.setCursorPosition(leadingEdge); + updateOrigin(); } private synchronized void moveCursorToNextRowIfNecessary() { @@ -338,13 +347,7 @@ public class LispTerminal { public void finish() { isFinished = true; - - try { - inputWriter.close(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } + inputWriter.close(); } } diff --git a/src/terminal/SafePipedStream.java b/src/terminal/SafePipedStream.java new file mode 100644 index 0000000..334365b --- /dev/null +++ b/src/terminal/SafePipedStream.java @@ -0,0 +1,81 @@ +package terminal; + +import java.io.*; + +public interface SafePipedStream { + + public static class SafePipedInputStream { + + private PipedInputStream underlyingStream; + + public SafePipedInputStream(PipedInputStream underlyingStream) { + this.underlyingStream = underlyingStream; + } + + public int read() { + try { + return underlyingStream.read(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public void close() { + try { + underlyingStream.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + public static class SafePipedOutputStream { + + private PipedOutputStream underlyingStream; + + public SafePipedOutputStream(PipedOutputStream underlyingStream) { + this.underlyingStream = underlyingStream; + } + + public void write(byte[] b) { + try { + underlyingStream.write(b); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public void flush() { + + try { + underlyingStream.flush(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public void close() { + try { + underlyingStream.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + public static class UncheckedIOException extends RuntimeException { + + private static final long serialVersionUID = 1L; + private IOException exception; + + public UncheckedIOException(IOException exception) { + this.exception = exception; + } + + @Override + public String getMessage() { + return exception.getMessage(); + } + } + +} diff --git a/test/terminal/LispTerminalTest.java b/test/terminal/LispTerminalTest.java index 3a554d2..68d944a 100644 --- a/test/terminal/LispTerminalTest.java +++ b/test/terminal/LispTerminalTest.java @@ -26,6 +26,13 @@ public class LispTerminalTest { waitForFlushes(1); } + private void pressKeyTimes(KeyType keyType, int times) { + for (int i = times; i > 0; i--) + virtualTerminal.addInput(new KeyStroke(keyType)); + + waitForFlushes(times); + } + private void pressControlKey(KeyType keyType) { virtualTerminal.addInput(new KeyStroke(keyType, true, false)); waitForFlushes(1); @@ -56,7 +63,7 @@ public class LispTerminalTest { outputWriter.write(END_OF_SEGMENT); outputWriter.flush(); waitForFlushes(1); - } catch (IOException e) {} + } catch (IOException ignored) {} } private void waitForFlushes(int flushCount) { @@ -67,7 +74,7 @@ public class LispTerminalTest { flushListener.resetFlushCount(); } - } catch (InterruptedException e) {} + } catch (InterruptedException ignored) {} } private void setColumns(int columns) { @@ -85,7 +92,7 @@ public class LispTerminalTest { private void assertCharacterAtPosition(char character, int column, int row) { TerminalPosition position = new TerminalPosition(column, row); - assertEquals(character, virtualTerminal.getCharacter(position).getCharacter()); + assertEquals(String.valueOf(character), String.valueOf(virtualTerminal.getCharacter(position).getCharacter())); } private void assertInputWritten(String expected) { @@ -97,7 +104,7 @@ public class LispTerminalTest { for (int c = inputReader.read(); c != -1; c = inputReader.read()) { actual += (char) c; } - } catch (IOException e) {} + } catch (IOException ignored) {} assertEquals(expected, actual); } @@ -106,7 +113,13 @@ public class LispTerminalTest { try { inputWriter.write(0); fail("input stream not closed"); - } catch (IOException e) {} + } catch (IOException ignored) {} + } + + private void assertCharacterPositions(char[][] positions) { + for (int row = 0; row < positions.length; row++) + for (int column = 0; column < positions[row].length; column++) + assertCharacterAtPosition(positions[row][column], column, row); } @Before @@ -153,8 +166,7 @@ public class LispTerminalTest { setColumns(5); enterCharacters("123451"); assertCursorPosition(1, 1); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); + pressKeyTimes(KeyType.ArrowLeft, 2); assertCursorPosition(4, 0); } @@ -181,13 +193,9 @@ public class LispTerminalTest { setColumns(5); enterCharacters("123451"); assertCursorPosition(1, 1); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); + pressKeyTimes(KeyType.ArrowLeft, 3); assertCursorPosition(3, 0); - pressKey(KeyType.ArrowRight); - pressKey(KeyType.ArrowRight); - pressKey(KeyType.ArrowRight); + pressKeyTimes(KeyType.ArrowRight, 3); assertCursorPosition(1, 1); } @@ -201,8 +209,7 @@ public class LispTerminalTest { @Test public void characterIsInserted() { enterCharacters("abcd"); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); + pressKeyTimes(KeyType.ArrowLeft, 2); enterCharacter('x'); assertCharacterAtPosition('a', 0, 0); assertCharacterAtPosition('b', 1, 0); @@ -215,8 +222,7 @@ public class LispTerminalTest { public void characterIsInserted_PushesInputToNextRow() { setColumns(4); enterCharacters("abcd"); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); + pressKeyTimes(KeyType.ArrowLeft, 2); enterCharacter('x'); assertCharacterAtPosition('a', 0, 0); assertCharacterAtPosition('b', 1, 0); @@ -234,8 +240,7 @@ public class LispTerminalTest { @Test public void backspaceWorksAfterInput() { enterCharacters("12345"); - pressKey(KeyType.Backspace); - pressKey(KeyType.Backspace); + pressKeyTimes(KeyType.Backspace, 2); assertCursorPosition(3, 0); assertCharacterAtPosition('1', 0, 0); assertCharacterAtPosition('2', 1, 0); @@ -249,11 +254,7 @@ public class LispTerminalTest { public void backspaceWorksAcrossRow() { setColumns(4); enterCharacters("1234567"); - pressKey(KeyType.Backspace); - pressKey(KeyType.Backspace); - pressKey(KeyType.Backspace); - pressKey(KeyType.Backspace); - pressKey(KeyType.Backspace); + pressKeyTimes(KeyType.Backspace, 5); assertCursorPosition(2, 0); assertCharacterAtPosition('1', 0, 0); assertCharacterAtPosition('2', 1, 0); @@ -267,8 +268,7 @@ public class LispTerminalTest { @Test public void backspaceWorksInMiddleOfInput() { enterCharacters("12345"); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); + pressKeyTimes(KeyType.ArrowLeft, 2); pressKey(KeyType.Backspace); assertCursorPosition(2, 0); assertCharacterAtPosition('1', 0, 0); @@ -296,12 +296,8 @@ public class LispTerminalTest { @Test public void deleteWorksAtStartOfInput() { enterCharacters("del"); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.Delete); - pressKey(KeyType.Delete); - pressKey(KeyType.Delete); + pressKeyTimes(KeyType.ArrowLeft, 3); + pressKeyTimes(KeyType.Delete, 3); assertCursorPosition(0, 0); assertCharacterAtPosition(' ', 0, 0); assertCharacterAtPosition(' ', 1, 0); @@ -312,11 +308,7 @@ public class LispTerminalTest { public void deleteWorksAcrossRow() { setColumns(4); enterCharacters("delete"); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); + pressKeyTimes(KeyType.ArrowLeft, 5); pressKey(KeyType.Delete); assertCursorPosition(1, 0); assertCharacterAtPosition('d', 0, 0); @@ -343,8 +335,7 @@ public class LispTerminalTest { @Test public void enterPressedInMiddleOfInput_WritesEntireLineToPipedStream() { enterCharacters("enter"); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); + pressKeyTimes(KeyType.ArrowLeft, 2); pressKey(KeyType.Enter); assertInputWritten("enter\n"); } @@ -360,8 +351,7 @@ public class LispTerminalTest { @Test public void controlDWorksInMiddleOfInput() { enterCharacters("control-d"); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); + pressKeyTimes(KeyType.ArrowLeft, 2); enterControlCharacter('d'); assertInputStreamClosed(); assertInputWritten("control-d\n"); @@ -392,21 +382,14 @@ public class LispTerminalTest { public void outputIsWritten() { produceOutput("output"); assertCursorPosition(6, 0); - assertCharacterAtPosition('o', 0, 0); - assertCharacterAtPosition('u', 1, 0); - assertCharacterAtPosition('t', 2, 0); - assertCharacterAtPosition('p', 3, 0); - assertCharacterAtPosition('u', 4, 0); - assertCharacterAtPosition('t', 5, 0); + assertCharacterPositions(new char[][] { { 'o', 'u', 't', 'p', 'u', 't' } }); } @Test public void endOfSegmentCharacterIsNotPrinted() { produceOutput("> " + END_OF_SEGMENT); assertCursorPosition(2, 0); - assertCharacterAtPosition('>', 0, 0); - assertCharacterAtPosition(' ', 1, 0); - assertCharacterAtPosition(' ', 2, 0); + assertCharacterPositions(new char[][] { { '>', ' ', ' ' } }); } @Test @@ -415,29 +398,18 @@ public class LispTerminalTest { setRows(2); enterCharacters("01201201"); assertCursorPosition(2, 1); - assertCharacterAtPosition('0', 0, 0); - assertCharacterAtPosition('1', 1, 0); - assertCharacterAtPosition('2', 2, 0); - assertCharacterAtPosition('0', 0, 1); - assertCharacterAtPosition('1', 1, 1); + assertCharacterPositions(new char[][] { { '0', '1', '2' }, { '0', '1', ' ' } }); } // @Test // public void insertingTextPushesInputPastEndOfBuffer() { -// setColumns(2); +// setColumns(3); // setRows(3); -// enterCharacters("00112"); -// -// pressKey(KeyType.ArrowLeft); -// pressKey(KeyType.ArrowLeft); -// pressKey(KeyType.ArrowLeft); +// enterCharacters("00011122"); +// pressKeyTimes(KeyType.ArrowLeft, 4); // enterCharacters("zz"); // assertCursorPosition(0, 1); -// assertCharacterAtPosition('z', 0, 0); -// assertCharacterAtPosition('z', 1, 0); -// assertCharacterAtPosition('1', 0, 1); -// assertCharacterAtPosition('1', 1, 1); -// assertCharacterAtPosition('2', 0, 2); +// assertCharacterPositions(new char[][] { { '1', 'z', 'z' }, { '1', '1', '2' }, { '2', ' ', ' ' } }); // } @Test @@ -453,64 +425,30 @@ public class LispTerminalTest { setRows(2); produceOutput("output"); assertCursorPosition(0, 1); - assertCharacterAtPosition('p', 0, 0); - assertCharacterAtPosition('u', 1, 0); - assertCharacterAtPosition('t', 2, 0); - assertCharacterAtPosition(' ', 0, 1); - assertCharacterAtPosition(' ', 1, 1); - assertCharacterAtPosition(' ', 2, 1); + assertCharacterPositions(new char[][] { { 'p', 'u', 't' }, { ' ', ' ', ' ' } }); } @Test public void printedOutputDoesNotOverwriteInput() { setColumns(3); enterCharacters("01201201"); - - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); + pressKeyTimes(KeyType.ArrowLeft, 5); produceOutput("out"); assertCursorPosition(0, 4); - assertCharacterAtPosition('0', 0, 0); - assertCharacterAtPosition('1', 1, 0); - assertCharacterAtPosition('2', 2, 0); - assertCharacterAtPosition('0', 0, 1); - assertCharacterAtPosition('1', 1, 1); - assertCharacterAtPosition('2', 2, 1); - assertCharacterAtPosition('0', 0, 2); - assertCharacterAtPosition('1', 1, 2); - assertCharacterAtPosition('o', 2, 2); - assertCharacterAtPosition('u', 0, 3); - assertCharacterAtPosition('t', 1, 3); - assertCharacterAtPosition(' ', 2, 3); + assertCharacterPositions(new char[][] { { '0', '1', '2' }, { '0', '1', '2' }, { '0', '1', 'o' }, + { 'u', 't', ' ' } }); } + @Test public void printedOutputDoesNotOverwriteInput_AfterEnter() { setColumns(3); enterCharacters("01201201"); - - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); - pressKey(KeyType.ArrowLeft); + pressKeyTimes(KeyType.ArrowLeft, 5); pressKey(KeyType.Enter); produceOutput("out"); assertCursorPosition(0, 4); - assertCharacterAtPosition('0', 0, 0); - assertCharacterAtPosition('1', 1, 0); - assertCharacterAtPosition('2', 2, 0); - assertCharacterAtPosition('0', 0, 1); - assertCharacterAtPosition('1', 1, 1); - assertCharacterAtPosition('2', 2, 1); - assertCharacterAtPosition('0', 0, 2); - assertCharacterAtPosition('1', 1, 2); - assertCharacterAtPosition(' ', 2, 2); - assertCharacterAtPosition('o', 0, 3); - assertCharacterAtPosition('u', 1, 3); - assertCharacterAtPosition('t', 2, 3); + assertCharacterPositions(new char[][] { { '0', '1', '2' }, { '0', '1', '2' }, { '0', '1', ' ' }, + { 'o', 'u', 't' } }); } }