Add history feature to the terminal

The history is accessed through the up/down arrow keys.

Resolves #8
This commit is contained in:
Mike Cifelli 2017-04-04 15:37:57 -04:00
parent 243f8a83ac
commit 63fd890ac6
4 changed files with 214 additions and 37 deletions

View File

@ -22,6 +22,7 @@ public class LispTerminal {
private SafeOutputStream inputWriter; private SafeOutputStream inputWriter;
private SafeInputStream outputReader; private SafeInputStream outputReader;
private ControlSequenceHandler controlSequenceHandler; private ControlSequenceHandler controlSequenceHandler;
private TerminalHistory history;
private ExecutorService executorService; private ExecutorService executorService;
private TerminalSize terminalSize; private TerminalSize terminalSize;
private String inputLine; private String inputLine;
@ -35,6 +36,7 @@ public class LispTerminal {
this.inputWriter = new SafeOutputStream(configuration.getInputWriter()); this.inputWriter = new SafeOutputStream(configuration.getInputWriter());
this.outputReader = new SafeInputStream(configuration.getOutputReader()); this.outputReader = new SafeInputStream(configuration.getOutputReader());
this.controlSequenceHandler = new ControlSequenceHandler(); this.controlSequenceHandler = new ControlSequenceHandler();
this.history = new TerminalHistory();
this.executorService = Executors.newFixedThreadPool(2); this.executorService = Executors.newFixedThreadPool(2);
this.terminalSize = terminal.getTerminalSize(); this.terminalSize = terminal.getTerminalSize();
this.inputLine = ""; this.inputLine = "";
@ -141,7 +143,7 @@ public class LispTerminal {
doControlD(); doControlD();
} }
private void doControlC() { private synchronized void doControlC() {
moveCursorToEndOfInput(); moveCursorToEndOfInput();
terminal.putCharacter('\n'); terminal.putCharacter('\n');
inputLine = ""; inputLine = "";
@ -159,6 +161,10 @@ public class LispTerminal {
if (keyType == Enter) if (keyType == Enter)
doEnter(); doEnter();
else if (keyType == ArrowUp)
doUpArrow();
else if (keyType == ArrowDown)
doDownArrow();
else if (keyType == ArrowLeft) else if (keyType == ArrowLeft)
doLeftArrow(); doLeftArrow();
else if (keyType == ArrowRight) else if (keyType == ArrowRight)
@ -173,12 +179,73 @@ public class LispTerminal {
private synchronized void doEnter() { private synchronized void doEnter() {
moveCursorToEndOfInput(); moveCursorToEndOfInput();
history.addLine(inputLine);
terminal.putCharacter('\n'); terminal.putCharacter('\n');
writeInputLine();
setOriginToCurrentPosition();
}
private synchronized void writeInputLine() {
inputLine += "\n"; inputLine += "\n";
inputWriter.write(inputLine.getBytes()); inputWriter.write(inputLine.getBytes());
inputWriter.flush(); inputWriter.flush();
inputLine = ""; 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() { private synchronized void doLeftArrow() {
@ -390,7 +457,7 @@ public class LispTerminal {
return new SafeInputStream(new ByteArrayInputStream(outputSegment.getBytes())); return new SafeInputStream(new ByteArrayInputStream(outputSegment.getBytes()));
} }
private void applyControlSequence(SafeInputStream input) { private synchronized void applyControlSequence(SafeInputStream input) {
ControlSequence controlSequence = controlSequenceHandler.parse(input); ControlSequence controlSequence = controlSequenceHandler.parse(input);
controlSequence.applyTo(terminal); controlSequence.applyTo(terminal);
} }

View File

@ -5,38 +5,48 @@ import java.util.*;
public class TerminalHistory { public class TerminalHistory {
private List<String> history; private List<String> history;
private int currentLineIndex; private int lineIndex;
public TerminalHistory() { public TerminalHistory() {
this.history = new ArrayList<>(); this.history = new ArrayList<>();
this.currentLineIndex = 0; this.history.add("");
this.lineIndex = 0;
} }
public void addLine(String line) { public void addLine(String line) {
history.add(line); history.set(lastIndex(), line);
currentLineIndex = history.size(); history.add("");
lineIndex = lastIndex();
}
private int lastIndex() {
return history.size() - 1;
}
public void updateCurrentLine(String line) {
history.set(lineIndex, line);
} }
public String getPreviousLine() { public String getPreviousLine() {
if (isBeginning()) if (isBeginning())
return null; return history.get(lineIndex);
return history.get(--currentLineIndex); return history.get(--lineIndex);
} }
private boolean isBeginning() { public boolean isBeginning() {
return currentLineIndex == 0; return lineIndex == 0;
} }
public String getNextLine() { public String getNextLine() {
if (isEnd()) if (isEnd())
return null; return history.get(lineIndex);
return history.get(++currentLineIndex); return history.get(++lineIndex);
} }
private boolean isEnd() { public boolean isEnd() {
return currentLineIndex >= history.size() - 1; return lineIndex == lastIndex();
} }
} }

View File

@ -434,4 +434,70 @@ public class LispTerminalTest {
terminal.assertCharacterPositions(new char[][] { { 'c', 'o', 'n', 't', 'r', 'o', 'l', 's', 'e', 'q' } }); 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);
}
} }

View File

@ -9,11 +9,11 @@ public class TerminalHistoryTest {
private TerminalHistory history; private TerminalHistory history;
private void assertAtBeginning() { private void assertAtBeginning() {
assertNull(history.getPreviousLine()); assertTrue(history.isBeginning());
} }
private void assertAtEnd() { private void assertAtEnd() {
assertNull(history.getNextLine()); assertTrue(history.isEnd());
} }
private void assertPrevious(String expected) { private void assertPrevious(String expected) {
@ -44,7 +44,14 @@ public class TerminalHistoryTest {
assertPrevious("test line"); assertPrevious("test line");
assertAtBeginning(); assertAtBeginning();
assertAtEnd(); }
@Test
public void currentLineIsEmpty() {
history.addLine("one");
history.getPreviousLine();
assertNext("");
} }
@Test @Test
@ -65,24 +72,7 @@ public class TerminalHistoryTest {
history.getPreviousLine(); history.getPreviousLine();
assertNext("two"); assertNext("two");
assertAtEnd(); assertNext("");
}
@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");
assertAtEnd(); assertAtEnd();
} }
@ -100,4 +90,48 @@ public class TerminalHistoryTest {
assertAtBeginning(); 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");
}
} }