From 63fd890ac698b96c7b9a3334848a4cc424ffa712 Mon Sep 17 00:00:00 2001 From: Mike Cifelli Date: Tue, 4 Apr 2017 15:37:57 -0400 Subject: [PATCH] Add history feature to the terminal The history is accessed through the up/down arrow keys. Resolves #8 --- src/terminal/LispTerminal.java | 73 ++++++++++++++++++++++++- src/terminal/TerminalHistory.java | 36 +++++++----- test/terminal/LispTerminalTest.java | 66 ++++++++++++++++++++++ test/terminal/TerminalHistoryTest.java | 76 +++++++++++++++++++------- 4 files changed, 214 insertions(+), 37 deletions(-) diff --git a/src/terminal/LispTerminal.java b/src/terminal/LispTerminal.java index f0c5af7..b6dec0f 100644 --- a/src/terminal/LispTerminal.java +++ b/src/terminal/LispTerminal.java @@ -22,6 +22,7 @@ public class LispTerminal { private SafeOutputStream inputWriter; private SafeInputStream outputReader; private ControlSequenceHandler controlSequenceHandler; + private TerminalHistory history; private ExecutorService executorService; private TerminalSize terminalSize; private String inputLine; @@ -35,6 +36,7 @@ public class LispTerminal { this.inputWriter = new SafeOutputStream(configuration.getInputWriter()); this.outputReader = new SafeInputStream(configuration.getOutputReader()); this.controlSequenceHandler = new ControlSequenceHandler(); + this.history = new TerminalHistory(); this.executorService = Executors.newFixedThreadPool(2); this.terminalSize = terminal.getTerminalSize(); this.inputLine = ""; @@ -141,7 +143,7 @@ public class LispTerminal { doControlD(); } - private void doControlC() { + private synchronized void doControlC() { moveCursorToEndOfInput(); terminal.putCharacter('\n'); inputLine = ""; @@ -159,6 +161,10 @@ public class LispTerminal { if (keyType == Enter) doEnter(); + else if (keyType == ArrowUp) + doUpArrow(); + else if (keyType == ArrowDown) + doDownArrow(); else if (keyType == ArrowLeft) doLeftArrow(); else if (keyType == ArrowRight) @@ -173,12 +179,73 @@ public class LispTerminal { private synchronized void doEnter() { moveCursorToEndOfInput(); + history.addLine(inputLine); terminal.putCharacter('\n'); + writeInputLine(); + setOriginToCurrentPosition(); + } + + private synchronized void writeInputLine() { inputLine += "\n"; inputWriter.write(inputLine.getBytes()); inputWriter.flush(); inputLine = ""; - setOriginToCurrentPosition(); + } + + private synchronized void doUpArrow() { + if (!history.isBeginning()) + replaceInputWithPreviousLine(); + } + + private synchronized void replaceInputWithPreviousLine() { + history.updateCurrentLine(inputLine); + clearInput(); + displayPreviousInputLine(); + } + + private synchronized void clearInput() { + terminal.setCursorPosition(originColumn, originRow); + putStringToTerminal(inputLine.replaceAll(".{1}", " ")); + terminal.setCursorPosition(originColumn, originRow); + } + + private synchronized void displayPreviousInputLine() { + inputLine = history.getPreviousLine(); + char[] inputCharacters = inputLine.toCharArray(); + + for (int i = 0; i < inputCharacters.length; i++) + displayCharacter(inputCharacters[i], i); + } + + private synchronized void displayCharacter(char character, int offsetFromOrigin) { + if (isLastColumn(offsetFromOrigin)) { + TerminalPosition cursorPosition = terminal.getCursorPosition(); + terminal.putCharacter(character); + moveCursorToNextRow(cursorPosition); + } else + terminal.putCharacter(character); + } + + private synchronized boolean isLastColumn(int offsetFromOrigin) { + int totalColumns = terminalSize.getColumns(); + + return (originColumn + offsetFromOrigin) % totalColumns == totalColumns - 1; + } + + private synchronized void doDownArrow() { + if (!history.isEnd()) + replaceInputWithNextLine(); + } + + private synchronized void replaceInputWithNextLine() { + history.updateCurrentLine(inputLine); + clearInput(); + displayNextInputLine(); + } + + private synchronized void displayNextInputLine() { + inputLine = history.getNextLine(); + putStringToTerminal(inputLine); } private synchronized void doLeftArrow() { @@ -390,7 +457,7 @@ public class LispTerminal { return new SafeInputStream(new ByteArrayInputStream(outputSegment.getBytes())); } - private void applyControlSequence(SafeInputStream input) { + private synchronized void applyControlSequence(SafeInputStream input) { ControlSequence controlSequence = controlSequenceHandler.parse(input); controlSequence.applyTo(terminal); } diff --git a/src/terminal/TerminalHistory.java b/src/terminal/TerminalHistory.java index cd7264c..9185d20 100644 --- a/src/terminal/TerminalHistory.java +++ b/src/terminal/TerminalHistory.java @@ -5,38 +5,48 @@ import java.util.*; public class TerminalHistory { private List history; - private int currentLineIndex; + private int lineIndex; public TerminalHistory() { this.history = new ArrayList<>(); - this.currentLineIndex = 0; + this.history.add(""); + this.lineIndex = 0; } public void addLine(String line) { - history.add(line); - currentLineIndex = history.size(); + history.set(lastIndex(), line); + history.add(""); + lineIndex = lastIndex(); + } + + private int lastIndex() { + return history.size() - 1; + } + + public void updateCurrentLine(String line) { + history.set(lineIndex, line); } public String getPreviousLine() { if (isBeginning()) - return null; - - return history.get(--currentLineIndex); + return history.get(lineIndex); + + return history.get(--lineIndex); } - private boolean isBeginning() { - return currentLineIndex == 0; + public boolean isBeginning() { + return lineIndex == 0; } public String getNextLine() { if (isEnd()) - return null; + return history.get(lineIndex); - return history.get(++currentLineIndex); + return history.get(++lineIndex); } - private boolean isEnd() { - return currentLineIndex >= history.size() - 1; + public boolean isEnd() { + return lineIndex == lastIndex(); } } diff --git a/test/terminal/LispTerminalTest.java b/test/terminal/LispTerminalTest.java index 29a5077..faad97e 100644 --- a/test/terminal/LispTerminalTest.java +++ b/test/terminal/LispTerminalTest.java @@ -434,4 +434,70 @@ public class LispTerminalTest { terminal.assertCharacterPositions(new char[][] { { 'c', 'o', 'n', 't', 'r', 'o', 'l', 's', 'e', 'q' } }); } + @Test + public void upArrowWorks() { + terminal.enterCharacters("(one)"); + terminal.pressKey(Enter); + terminal.enterCharacters("(two)"); + terminal.pressKey(Enter); + terminal.pressKey(ArrowUp); + terminal.assertScreenText("(one)", "(two)", "(two)"); + terminal.assertCursorPosition(5, 2); + } + + @Test + public void upArrowErasesCurrentLine() { + terminal.enterCharacters("(one)"); + terminal.pressKey(Enter); + terminal.enterCharacters("(two)"); + terminal.pressKey(Enter); + terminal.enterCharacters("(three)"); + terminal.pressKey(ArrowUp); + terminal.assertScreenText("(one)", "(two)", "(two) "); + terminal.assertCursorPosition(5, 2); + } + + @Test + public void upArrowStopsAtFirstLine() { + terminal.enterCharacters("(one)"); + terminal.pressKey(Enter); + terminal.enterCharacters("(two)"); + terminal.pressKey(Enter); + terminal.pressKeyTimes(ArrowUp, 5); + terminal.assertScreenText("(one)", "(two)", "(one)"); + terminal.assertCursorPosition(5, 2); + } + + @Test + public void originIsUpdatedWhenPreviousLineMovesPastEndOfBuffer() { + terminal.setRows(3); + terminal.setColumns(3); + terminal.enterCharacters("12345"); + terminal.pressKey(Enter); + terminal.pressKey(ArrowUp); + terminal.assertScreenText("45 ", "123", "45 "); + terminal.pressKeyTimes(ArrowLeft, 10); + terminal.assertCursorPosition(0, 1); + } + + @Test + public void downArrowWorks() { + terminal.enterCharacters("(one)"); + terminal.pressKey(Enter); + terminal.enterCharacters("(two)"); + terminal.pressKey(Enter); + terminal.enterCharacters("(three)"); + terminal.pressKey(ArrowUp); + terminal.pressKey(ArrowDown); + terminal.assertScreenText("(one)", "(two)", "(three)"); + terminal.assertCursorPosition(7, 2); + } + + @Test + public void downArrowStopsAtLastLine() { + terminal.pressKeyTimes(ArrowDown, 5); + terminal.assertScreenText(" "); + terminal.assertCursorPosition(0, 0); + } + } diff --git a/test/terminal/TerminalHistoryTest.java b/test/terminal/TerminalHistoryTest.java index ec25303..1577ecf 100644 --- a/test/terminal/TerminalHistoryTest.java +++ b/test/terminal/TerminalHistoryTest.java @@ -9,11 +9,11 @@ public class TerminalHistoryTest { private TerminalHistory history; private void assertAtBeginning() { - assertNull(history.getPreviousLine()); + assertTrue(history.isBeginning()); } private void assertAtEnd() { - assertNull(history.getNextLine()); + assertTrue(history.isEnd()); } private void assertPrevious(String expected) { @@ -44,7 +44,14 @@ public class TerminalHistoryTest { assertPrevious("test line"); assertAtBeginning(); - assertAtEnd(); + } + + @Test + public void currentLineIsEmpty() { + history.addLine("one"); + history.getPreviousLine(); + + assertNext(""); } @Test @@ -65,24 +72,7 @@ public class TerminalHistoryTest { history.getPreviousLine(); assertNext("two"); - assertAtEnd(); - } - - @Test - public void moveBackAndForth() { - history.addLine("one"); - history.addLine("two"); - - assertAtEnd(); - assertPrevious("two"); - assertAtEnd(); - assertPrevious("one"); - assertAtBeginning(); - assertNext("two"); - assertAtEnd(); - assertPrevious("one"); - assertAtBeginning(); - assertNext("two"); + assertNext(""); assertAtEnd(); } @@ -100,4 +90,48 @@ public class TerminalHistoryTest { assertAtBeginning(); } + @Test + public void currentLineIsUpdated() { + history.addLine("one"); + history.updateCurrentLine("purple"); + history.getPreviousLine(); + + assertNext("purple"); + } + + @Test + public void previousLineIsUpdated() { + history.addLine("one"); + history.addLine("two"); + history.getPreviousLine(); + history.updateCurrentLine("purple"); + history.getPreviousLine(); + + assertNext("purple"); + } + + @Test + public void goingPastFirstLine_KeepsReturningFirstLine() { + history.addLine("one"); + history.addLine("two"); + history.getPreviousLine(); + history.getPreviousLine(); + history.getPreviousLine(); + history.getPreviousLine(); + history.getPreviousLine(); + + assertPrevious("one"); + } + + @Test + public void goingPastLastLine_KeepsReturningLastLine() { + history.updateCurrentLine("current"); + history.getNextLine(); + history.getNextLine(); + history.getNextLine(); + history.getNextLine(); + + assertNext("current"); + } + }