diff --git a/pom.xml b/pom.xml index 1defd19..a27c2d3 100644 --- a/pom.xml +++ b/pom.xml @@ -211,7 +211,7 @@ com.github.stefanbirkner system-rules - 1.16.1 + 1.18.0 test diff --git a/src/main/kotlin/application/LispMain.kt b/src/main/kotlin/application/LispMain.kt index 70bcdd5..f4be730 100644 --- a/src/main/kotlin/application/LispMain.kt +++ b/src/main/kotlin/application/LispMain.kt @@ -5,7 +5,7 @@ import com.googlecode.lanterna.terminal.IOSafeTerminalAdapter.createRuntimeExcep import interpreter.LispInterpreter import interpreter.LispInterpreterBuilder import terminal.LispTerminal -import terminal.LispTerminal.END_OF_SEGMENT +import terminal.LispTerminal.Companion.END_OF_SEGMENT import terminal.TerminalConfiguration import java.io.PipedInputStream import java.io.PipedOutputStream diff --git a/src/main/kotlin/terminal/LispTerminal.java b/src/main/kotlin/terminal/LispTerminal.java deleted file mode 100644 index 9fb0ed6..0000000 --- a/src/main/kotlin/terminal/LispTerminal.java +++ /dev/null @@ -1,460 +0,0 @@ -package terminal; - -import com.googlecode.lanterna.TerminalPosition; -import com.googlecode.lanterna.TerminalSize; -import com.googlecode.lanterna.input.KeyStroke; -import com.googlecode.lanterna.input.KeyType; -import com.googlecode.lanterna.terminal.IOSafeTerminal; -import stream.SafeInputStream; -import stream.SafeOutputStream; -import util.Characters; - -import java.io.ByteArrayInputStream; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import static com.googlecode.lanterna.input.KeyType.ArrowDown; -import static com.googlecode.lanterna.input.KeyType.ArrowLeft; -import static com.googlecode.lanterna.input.KeyType.ArrowRight; -import static com.googlecode.lanterna.input.KeyType.ArrowUp; -import static com.googlecode.lanterna.input.KeyType.Backspace; -import static com.googlecode.lanterna.input.KeyType.Character; -import static com.googlecode.lanterna.input.KeyType.Delete; -import static com.googlecode.lanterna.input.KeyType.Enter; -import static terminal.ControlSequenceHandler.isEscape; - -public class LispTerminal { - - public static final char END_OF_SEGMENT = Characters.UNICODE_NULL; - - private IOSafeTerminal terminal; - private SafeOutputStream inputWriter; - private SafeInputStream outputReader; - private ControlSequenceHandler controlSequenceHandler; - private TerminalHistory history; - private ExecutorService executorService; - private TerminalSize terminalSize; - private String inputLine; - private String outputSegment; - private boolean isStopped; - private int originColumn; - private int originRow; - - public LispTerminal(TerminalConfiguration configuration) { - this.terminal = configuration.getTerminal(); - 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 = ""; - this.outputSegment = ""; - this.isStopped = false; - - setOriginToCurrentPosition(); - terminal.addResizeListener((t, newSize) -> resize()); - } - - private synchronized void setOriginToCurrentPosition() { - TerminalPosition cursorPosition = terminal.getCursorPosition(); - originColumn = cursorPosition.getColumn(); - originRow = cursorPosition.getRow(); - } - - private synchronized void resize() { - terminalSize = terminal.getTerminalSize(); - terminal.clearScreen(); - terminal.setCursorPosition(0, 0); - redisplayInput(); - } - - private synchronized void redisplayInput() { - setOriginToCurrentPosition(); - putStringToTerminal(inputLine); - moveCursorToEndOfInput(); - } - - private synchronized void putStringToTerminal(String characters) { - for (char c : characters.toCharArray()) - terminal.putCharacter(c); - } - - private synchronized void moveCursorToEndOfInput() { - terminal.setCursorPosition(getLeadingEdge()); - } - - 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); - } - - public void start() { - executorService.execute(this::readInput); - executorService.execute(this::writeOutput); - executorService.shutdown(); - } - - private void readInput() { - while (!isStopped) - processNextKey(); - } - - private void processNextKey() { - KeyStroke keyStroke = terminal.pollInput(); - - if (keyStroke != null) - handleKey(keyStroke); - else - takeNap(); - } - - private synchronized void handleKey(KeyStroke keyStroke) { - doKey(keyStroke); - terminal.flush(); - } - - private synchronized void doKey(KeyStroke keyStroke) { - if (keyStroke.isCtrlDown()) - doControlKey(keyStroke); - else - doNormalKey(keyStroke); - } - - private synchronized void doControlKey(KeyStroke keyStroke) { - KeyType keyType = keyStroke.getKeyType(); - - if (keyType == Character) - doControlCharacter(keyStroke); - } - - private synchronized void doControlCharacter(KeyStroke keyStroke) { - if (keyStroke.getCharacter() == 'd') - doControlD(); - } - - private synchronized void doControlD() { - doEnter(); - stop(); - } - - private synchronized void doNormalKey(KeyStroke keyStroke) { - KeyType keyType = keyStroke.getKeyType(); - - if (keyType == Enter) - doEnter(); - else if (keyType == ArrowUp) - doUpArrow(); - else if (keyType == ArrowDown) - doDownArrow(); - else if (keyType == ArrowLeft) - doLeftArrow(); - else if (keyType == ArrowRight) - doRightArrow(); - else if (keyType == Backspace) - doBackspace(); - else if (keyType == Delete) - doDelete(); - else if (keyType == Character) - doCharacter(keyStroke.getCharacter()); - } - - 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 = ""; - } - - 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() { - TerminalPosition cursorPosition = terminal.getCursorPosition(); - - if (isPossibleToMoveLeft(cursorPosition)) - moveCursorLeft(cursorPosition); - } - - private synchronized boolean isPossibleToMoveLeft(TerminalPosition cursorPosition) { - return getDistanceFromOrigin(cursorPosition) > 0; - } - - 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 void moveCursorLeft(TerminalPosition cursorPosition) { - TerminalPosition newPosition = cursorPosition.withRelativeColumn(-1); - - if (isAtStartOfRow(cursorPosition)) - newPosition = cursorPosition.withColumn(terminalSize.getColumns()).withRelativeRow(-1); - - terminal.setCursorPosition(newPosition); - } - - private synchronized boolean isAtStartOfRow(TerminalPosition cursorPosition) { - return cursorPosition.getColumn() == 0; - } - - private synchronized void doRightArrow() { - TerminalPosition cursorPosition = terminal.getCursorPosition(); - - if (isPossibleToMoveRight(cursorPosition)) - moveCursorRight(cursorPosition); - } - - private synchronized boolean isPossibleToMoveRight(TerminalPosition cursorPosition) { - return getDistanceFromOrigin(cursorPosition) < inputLine.length(); - } - - private synchronized void moveCursorRight(TerminalPosition cursorPosition) { - if (isEndOfRow(cursorPosition)) - moveCursorToNextRow(cursorPosition); - else - terminal.setCursorPosition(cursorPosition.withRelativeColumn(1)); - } - - private synchronized boolean isEndOfRow(TerminalPosition cursorPosition) { - return cursorPosition.getColumn() >= terminalSize.getColumns() - 1; - } - - private synchronized void moveCursorToNextRow(TerminalPosition cursorPosition) { - if (isEndOfBuffer(cursorPosition)) - createNewRowForCursor(cursorPosition); - else - terminal.setCursorPosition(cursorPosition.withColumn(0).withRelativeRow(1)); - } - - private synchronized boolean isEndOfBuffer(TerminalPosition cursorPosition) { - return cursorPosition.getRow() == terminalSize.getRows() - 1; - } - - private synchronized void createNewRowForCursor(TerminalPosition cursorPosition) { - terminal.setCursorPosition(cursorPosition); - terminal.putCharacter('\n'); - terminal.setCursorPosition(cursorPosition.withColumn(0)); - --originRow; - } - - private synchronized void doBackspace() { - TerminalPosition cursorPosition = terminal.getCursorPosition(); - - if (isPossibleToMoveLeft(cursorPosition)) - deletePreviousCharacter(cursorPosition); - } - - private synchronized void deletePreviousCharacter(TerminalPosition cursorPosition) { - int distanceFromOrigin = getDistanceFromOrigin(cursorPosition); - String remaining = inputLine.substring(distanceFromOrigin, inputLine.length()); - inputLine = inputLine.substring(0, distanceFromOrigin - 1) + remaining; - moveCursorLeft(cursorPosition); - putStringToTerminal(remaining + " "); - moveCursorLeft(cursorPosition); - } - - private synchronized void doDelete() { - TerminalPosition cursorPosition = terminal.getCursorPosition(); - - if (isPossibleToMoveRight(cursorPosition)) - deleteCharacterAtPosition(cursorPosition); - } - - private synchronized void deleteCharacterAtPosition(TerminalPosition cursorPosition) { - int distanceFromOrigin = getDistanceFromOrigin(cursorPosition); - String remaining = inputLine.substring(distanceFromOrigin + 1, inputLine.length()); - inputLine = inputLine.substring(0, distanceFromOrigin) + remaining; - putStringToTerminal(remaining + " "); - terminal.setCursorPosition(cursorPosition); - } - - private synchronized void doCharacter(Character character) { - TerminalPosition cursorPosition = terminal.getCursorPosition(); - - if (!isBufferFilled()) - if (isPossibleToMoveRight(cursorPosition)) - insertCharacter(character, cursorPosition); - else - appendCharacter(character, cursorPosition); - } - - private synchronized boolean isBufferFilled() { - int row = getLeadingEdge().getRow(); - int column = getLeadingEdge().getColumn(); - - return (row == terminalSize.getRows() - 1) && (column >= terminalSize.getColumns() - 1) && (originRow <= 0); - } - - private synchronized void insertCharacter(Character character, TerminalPosition cursorPosition) { - cursorPosition = shiftPositionIfNewRowAdded(cursorPosition); - terminal.setCursorPosition(cursorPosition); - int distanceFromOrigin = getDistanceFromOrigin(cursorPosition); - String remaining = character + inputLine.substring(distanceFromOrigin, inputLine.length()); - inputLine = inputLine.substring(0, distanceFromOrigin) + remaining; - putStringToTerminal(remaining); - moveCursorRight(cursorPosition); - } - - private synchronized TerminalPosition shiftPositionIfNewRowAdded(TerminalPosition cursorPosition) { - int oldOriginRow = originRow; - moveCursorRight(getLeadingEdge()); - - return isNewRowAdded(oldOriginRow) ? adjustCursorPosition(cursorPosition) : cursorPosition; - } - - private synchronized boolean isNewRowAdded(int oldOriginRow) { - return originRow != oldOriginRow; - } - - private synchronized TerminalPosition adjustCursorPosition(TerminalPosition cursorPosition) { - terminal.setCursorPosition(new TerminalPosition(originColumn, originRow)); - putStringToTerminal(inputLine); - - return cursorPosition.withRelativeRow(-1); - } - - private synchronized void appendCharacter(Character character, TerminalPosition cursorPosition) { - terminal.putCharacter(character); - inputLine += character; - moveCursorRight(cursorPosition); - } - - private void takeNap() { - try { - Thread.sleep(1); - } catch (InterruptedException ignored) { - } - } - - private void writeOutput() { - for (int c = outputReader.read(); c != Characters.EOF; c = outputReader.read()) - processOutput((char) c); - - terminal.flush(); - terminal.close(); - } - - private synchronized void processOutput(char c) { - if (isEndOfSegment(c)) - writeSegment(); - else - outputSegment += c; - } - - private synchronized boolean isEndOfSegment(char c) { - return c == END_OF_SEGMENT; - } - - private synchronized void writeSegment() { - printSegmentCharacters(); - outputSegment = ""; - redisplayInput(); - terminal.flush(); - } - - private synchronized void printSegmentCharacters() { - moveCursorToEndOfInput(); - putOutputToTerminal(); - moveCursorToNextRowIfNecessary(); - } - - private synchronized void putOutputToTerminal() { - SafeInputStream input = convertOutputToStream(); - - for (int c = input.read(); c != Characters.EOF; c = input.read()) - if (isEscape((char) c)) - applyControlSequence(input); - else - terminal.putCharacter((char) c); - } - - private synchronized SafeInputStream convertOutputToStream() { - return new SafeInputStream(new ByteArrayInputStream(outputSegment.getBytes())); - } - - private synchronized void applyControlSequence(SafeInputStream input) { - ControlSequence controlSequence = controlSequenceHandler.parse(input); - controlSequence.applyTo(terminal); - } - - private synchronized void moveCursorToNextRowIfNecessary() { - TerminalPosition cursorPosition = terminal.getCursorPosition(); - - if (isEndOfRow(cursorPosition)) - moveCursorToNextRow(cursorPosition); - } - - public void stop() { - isStopped = true; - inputWriter.close(); - } -} diff --git a/src/main/kotlin/terminal/LispTerminal.kt b/src/main/kotlin/terminal/LispTerminal.kt new file mode 100644 index 0000000..6dffa1b --- /dev/null +++ b/src/main/kotlin/terminal/LispTerminal.kt @@ -0,0 +1,494 @@ +package terminal + +import com.googlecode.lanterna.TerminalPosition +import com.googlecode.lanterna.input.KeyStroke +import com.googlecode.lanterna.input.KeyType.ArrowDown +import com.googlecode.lanterna.input.KeyType.ArrowLeft +import com.googlecode.lanterna.input.KeyType.ArrowRight +import com.googlecode.lanterna.input.KeyType.ArrowUp +import com.googlecode.lanterna.input.KeyType.Backspace +import com.googlecode.lanterna.input.KeyType.Character +import com.googlecode.lanterna.input.KeyType.Delete +import com.googlecode.lanterna.input.KeyType.Enter +import stream.SafeInputStream +import stream.SafeOutputStream +import terminal.ControlSequenceHandler.Companion.isEscape +import util.Characters +import util.Characters.EOF +import java.io.ByteArrayInputStream +import java.util.concurrent.Executors + +class LispTerminal(configuration: TerminalConfiguration) { + + private val terminal = configuration.terminal!! + private val inputWriter = SafeOutputStream(configuration.inputWriter) + private val outputReader = SafeInputStream(configuration.outputReader) + private val controlSequenceHandler = ControlSequenceHandler() + private val history = TerminalHistory() + private val executorService = Executors.newFixedThreadPool(2) + private var terminalSize = terminal.terminalSize + private var inputLine = "" + private var outputSegment = "" + private var isStopped = false + private var originColumn = 0 + private var originRow = 0 + + private val leadingEdge: TerminalPosition + @Synchronized get() { + val inputLength = inputLine.length + val totalColumns = terminalSize.columns + val rowDifference = inputLength / totalColumns + val columnDifference = inputLength % totalColumns + + return TerminalPosition(originColumn + columnDifference, originRow + rowDifference) + } + + private val isBufferFilled: Boolean + @Synchronized get() { + val row = leadingEdge.row + val column = leadingEdge.column + + return row == terminalSize.rows - 1 + && column >= terminalSize.columns - 1 + && originRow <= 0 + } + + init { + setOriginToCurrentPosition() + terminal.addResizeListener { _, _ -> resize() } + } + + @Synchronized + private fun setOriginToCurrentPosition() { + val cursorPosition = terminal.cursorPosition + originColumn = cursorPosition.column + originRow = cursorPosition.row + } + + @Synchronized + private fun resize() { + terminalSize = terminal.terminalSize + terminal.clearScreen() + terminal.setCursorPosition(0, 0) + redisplayInput() + } + + @Synchronized + private fun redisplayInput() { + setOriginToCurrentPosition() + putStringToTerminal(inputLine) + moveCursorToEndOfInput() + } + + @Synchronized + private fun putStringToTerminal(characters: String) { + for (c in characters.toCharArray()) + terminal.putCharacter(c) + } + + @Synchronized + private fun moveCursorToEndOfInput() { + terminal.cursorPosition = leadingEdge + } + + fun start() { + executorService.execute { this.readInput() } + executorService.execute { this.writeOutput() } + executorService.shutdown() + } + + private fun readInput() { + while (!isStopped) + processNextKey() + } + + private fun processNextKey() { + val keyStroke = terminal.pollInput() + + if (keyStroke != null) + handleKey(keyStroke) + else + takeNap() + } + + @Synchronized + private fun handleKey(keyStroke: KeyStroke) { + doKey(keyStroke) + terminal.flush() + } + + @Synchronized + private fun doKey(keyStroke: KeyStroke) { + if (keyStroke.isCtrlDown) + doControlKey(keyStroke) + else + doNormalKey(keyStroke) + } + + @Synchronized + private fun doControlKey(keyStroke: KeyStroke) { + if (keyStroke.keyType == Character) + doControlCharacter(keyStroke) + } + + @Synchronized + private fun doControlCharacter(keyStroke: KeyStroke) { + if (keyStroke.character == 'd') + doControlD() + } + + @Synchronized + private fun doControlD() { + doEnter() + stop() + } + + @Suppress("NON_EXHAUSTIVE_WHEN") + @Synchronized + private fun doNormalKey(keyStroke: KeyStroke) { + when (keyStroke.keyType) { + Enter -> doEnter() + ArrowUp -> doUpArrow() + ArrowDown -> doDownArrow() + ArrowLeft -> doLeftArrow() + ArrowRight -> doRightArrow() + Backspace -> doBackspace() + Delete -> doDelete() + Character -> doCharacter(keyStroke.character!!) + } + } + + @Synchronized + private fun doEnter() { + moveCursorToEndOfInput() + history.addLine(inputLine) + terminal.putCharacter('\n') + writeInputLine() + setOriginToCurrentPosition() + } + + @Synchronized + private fun writeInputLine() { + inputLine += "\n" + inputWriter.write(inputLine.toByteArray()) + inputWriter.flush() + inputLine = "" + } + + @Synchronized + private fun doUpArrow() { + if (!history.isBeginning()) + replaceInputWithPreviousLine() + } + + @Synchronized + private fun replaceInputWithPreviousLine() { + history.updateCurrentLine(inputLine) + clearInput() + displayPreviousInputLine() + } + + @Synchronized + private fun clearInput() { + terminal.setCursorPosition(originColumn, originRow) + putStringToTerminal(inputLine.replace(".".toRegex(), " ")) + terminal.setCursorPosition(originColumn, originRow) + } + + @Synchronized + private fun displayPreviousInputLine() { + inputLine = history.getPreviousLine() + val inputCharacters = inputLine.toCharArray() + + for (i in inputCharacters.indices) + displayCharacter(inputCharacters[i], i) + } + + @Synchronized + private fun displayCharacter(character: Char, offsetFromOrigin: Int) { + if (isLastColumn(offsetFromOrigin)) { + val cursorPosition = terminal.cursorPosition + terminal.putCharacter(character) + moveCursorToNextRow(cursorPosition) + } else + terminal.putCharacter(character) + } + + @Synchronized + private fun isLastColumn(offsetFromOrigin: Int): Boolean { + val totalColumns = terminalSize.columns + + return (originColumn + offsetFromOrigin) % totalColumns == totalColumns - 1 + } + + @Synchronized + private fun doDownArrow() { + if (!history.isEnd()) + replaceInputWithNextLine() + } + + @Synchronized + private fun replaceInputWithNextLine() { + history.updateCurrentLine(inputLine) + clearInput() + displayNextInputLine() + } + + @Synchronized + private fun displayNextInputLine() { + inputLine = history.getNextLine() + putStringToTerminal(inputLine) + } + + @Synchronized + private fun doLeftArrow() { + val cursorPosition = terminal.cursorPosition + + if (isPossibleToMoveLeft(cursorPosition)) + moveCursorLeft(cursorPosition) + } + + @Synchronized + private fun isPossibleToMoveLeft(cursorPosition: TerminalPosition): Boolean { + return getDistanceFromOrigin(cursorPosition) > 0 + } + + @Synchronized + private fun getDistanceFromOrigin(cursorPosition: TerminalPosition): Int { + val columnDifference = cursorPosition.column - originColumn + val rowDifference = cursorPosition.row - originRow + val totalColumns = terminalSize.columns + + return columnDifference + totalColumns * rowDifference + } + + @Synchronized + private fun moveCursorLeft(cursorPosition: TerminalPosition) { + var newPosition = cursorPosition.withRelativeColumn(-1) + + if (isAtStartOfRow(cursorPosition)) + newPosition = cursorPosition.withColumn(terminalSize.columns).withRelativeRow(-1) + + terminal.cursorPosition = newPosition + } + + @Synchronized + private fun isAtStartOfRow(cursorPosition: TerminalPosition): Boolean { + return cursorPosition.column == 0 + } + + @Synchronized + private fun doRightArrow() { + val cursorPosition = terminal.cursorPosition + + if (isPossibleToMoveRight(cursorPosition)) + moveCursorRight(cursorPosition) + } + + @Synchronized + private fun isPossibleToMoveRight(cursorPosition: TerminalPosition): Boolean { + return getDistanceFromOrigin(cursorPosition) < inputLine.length + } + + @Synchronized + private fun moveCursorRight(cursorPosition: TerminalPosition) { + if (isEndOfRow(cursorPosition)) + moveCursorToNextRow(cursorPosition) + else + terminal.cursorPosition = cursorPosition.withRelativeColumn(1) + } + + @Synchronized + private fun isEndOfRow(cursorPosition: TerminalPosition): Boolean { + return cursorPosition.column >= terminalSize.columns - 1 + } + + @Synchronized + private fun moveCursorToNextRow(cursorPosition: TerminalPosition) { + if (isEndOfBuffer(cursorPosition)) + createNewRowForCursor(cursorPosition) + else + terminal.cursorPosition = cursorPosition.withColumn(0).withRelativeRow(1) + } + + @Synchronized + private fun isEndOfBuffer(cursorPosition: TerminalPosition): Boolean { + return cursorPosition.row == terminalSize.rows - 1 + } + + @Synchronized + private fun createNewRowForCursor(cursorPosition: TerminalPosition) { + terminal.cursorPosition = cursorPosition + terminal.putCharacter('\n') + terminal.cursorPosition = cursorPosition.withColumn(0) + --originRow + } + + @Synchronized + private fun doBackspace() { + val cursorPosition = terminal.cursorPosition + + if (isPossibleToMoveLeft(cursorPosition)) + deletePreviousCharacter(cursorPosition) + } + + @Synchronized + private fun deletePreviousCharacter(cursorPosition: TerminalPosition) { + val distanceFromOrigin = getDistanceFromOrigin(cursorPosition) + val remaining = inputLine.substring(distanceFromOrigin, inputLine.length) + inputLine = inputLine.substring(0, distanceFromOrigin - 1) + remaining + moveCursorLeft(cursorPosition) + putStringToTerminal("$remaining ") + moveCursorLeft(cursorPosition) + } + + @Synchronized + private fun doDelete() { + val cursorPosition = terminal.cursorPosition + + if (isPossibleToMoveRight(cursorPosition)) + deleteCharacterAtPosition(cursorPosition) + } + + @Synchronized + private fun deleteCharacterAtPosition(cursorPosition: TerminalPosition) { + val distanceFromOrigin = getDistanceFromOrigin(cursorPosition) + val remaining = inputLine.substring(distanceFromOrigin + 1, inputLine.length) + inputLine = inputLine.substring(0, distanceFromOrigin) + remaining + putStringToTerminal("$remaining ") + terminal.cursorPosition = cursorPosition + } + + @Synchronized + private fun doCharacter(character: Char) { + val cursorPosition = terminal.cursorPosition + + if (!isBufferFilled) + if (isPossibleToMoveRight(cursorPosition)) + insertCharacter(character, cursorPosition) + else + appendCharacter(character, cursorPosition) + } + + @Synchronized + private fun insertCharacter(character: Char, cursorPosition: TerminalPosition) { + val shiftedPosition = shiftPositionIfNewRowAdded(cursorPosition) + terminal.cursorPosition = shiftedPosition + + val distanceFromOrigin = getDistanceFromOrigin(shiftedPosition) + val remaining = character + inputLine.substring(distanceFromOrigin, inputLine.length) + inputLine = inputLine.substring(0, distanceFromOrigin) + remaining + putStringToTerminal(remaining) + moveCursorRight(shiftedPosition) + } + + @Synchronized + private fun shiftPositionIfNewRowAdded(cursorPosition: TerminalPosition): TerminalPosition { + val oldOriginRow = originRow + moveCursorRight(leadingEdge) + + return if (isNewRowAdded(oldOriginRow)) adjustCursorPosition(cursorPosition) else cursorPosition + } + + @Synchronized + private fun isNewRowAdded(oldOriginRow: Int): Boolean { + return originRow != oldOriginRow + } + + @Synchronized + private fun adjustCursorPosition(cursorPosition: TerminalPosition): TerminalPosition { + terminal.cursorPosition = TerminalPosition(originColumn, originRow) + putStringToTerminal(inputLine) + + return cursorPosition.withRelativeRow(-1) + } + + @Synchronized + private fun appendCharacter(character: Char, cursorPosition: TerminalPosition) { + terminal.putCharacter(character) + inputLine += character + moveCursorRight(cursorPosition) + } + + private fun takeNap() { + Thread.sleep(1) + } + + private fun writeOutput() { + var c = outputReader.read() + + while (c != EOF) { + processOutput(c.toChar()) + c = outputReader.read() + } + + terminal.flush() + terminal.close() + } + + @Synchronized + private fun processOutput(c: Char) { + if (c == END_OF_SEGMENT) + writeSegment() + else + outputSegment += c + } + + @Synchronized + private fun writeSegment() { + printSegmentCharacters() + outputSegment = "" + redisplayInput() + terminal.flush() + } + + @Synchronized + private fun printSegmentCharacters() { + moveCursorToEndOfInput() + putOutputToTerminal() + moveCursorToNextRowIfNecessary() + } + + @Synchronized + private fun putOutputToTerminal() { + val input = convertOutputToStream() + var c = input.read() + + while (c != EOF) { + if (isEscape(c.toChar())) + applyControlSequence(input) + else + terminal.putCharacter(c.toChar()) + + c = input.read() + } + } + + @Synchronized + private fun convertOutputToStream(): SafeInputStream { + return SafeInputStream(ByteArrayInputStream(outputSegment.toByteArray())) + } + + @Synchronized + private fun applyControlSequence(input: SafeInputStream) { + val controlSequence = controlSequenceHandler.parse(input) + controlSequence.applyTo(terminal) + } + + @Synchronized + private fun moveCursorToNextRowIfNecessary() { + val cursorPosition = terminal.cursorPosition + + if (isEndOfRow(cursorPosition)) + moveCursorToNextRow(cursorPosition) + } + + fun stop() { + isStopped = true + inputWriter.close() + } + + companion object { + const val END_OF_SEGMENT = Characters.UNICODE_NULL + } +} diff --git a/src/test/kotlin/application/MainTest.kt b/src/test/kotlin/application/MainTest.kt index 24f1a93..97b30a8 100644 --- a/src/test/kotlin/application/MainTest.kt +++ b/src/test/kotlin/application/MainTest.kt @@ -13,6 +13,8 @@ import org.junit.Test import org.junit.contrib.java.lang.system.ExpectedSystemExit import org.junit.contrib.java.lang.system.SystemErrRule import org.junit.contrib.java.lang.system.SystemOutRule +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import terminal.TerminalConfiguration import terminal.VirtualTerminalInteractor import testutil.SymbolAndFunctionCleaner @@ -21,6 +23,7 @@ import java.io.PipedOutputStream import java.text.MessageFormat.format import java.util.concurrent.CountDownLatch +@TestInstance(PER_CLASS) class MainTest : SymbolAndFunctionCleaner() { companion object { diff --git a/src/test/kotlin/error/ErrorManagerTest.kt b/src/test/kotlin/error/ErrorManagerTest.kt index 5d02b12..130e615 100644 --- a/src/test/kotlin/error/ErrorManagerTest.kt +++ b/src/test/kotlin/error/ErrorManagerTest.kt @@ -8,9 +8,12 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import java.io.ByteArrayOutputStream import java.io.PrintStream +@TestInstance(PER_CLASS) class ErrorManagerTest { companion object { diff --git a/src/test/kotlin/function/ArgumentValidatorTest.kt b/src/test/kotlin/function/ArgumentValidatorTest.kt index 43060bf..f69f30f 100644 --- a/src/test/kotlin/function/ArgumentValidatorTest.kt +++ b/src/test/kotlin/function/ArgumentValidatorTest.kt @@ -7,6 +7,8 @@ import function.ArgumentValidator.TooManyArgumentsException import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import sexpression.Cons import sexpression.LispString import sexpression.Nil @@ -15,6 +17,7 @@ import sexpression.Symbol import sexpression.Symbol.Companion.T import testutil.TestUtilities.assertIsErrorWithMessage +@TestInstance(PER_CLASS) class ArgumentValidatorTest { companion object { diff --git a/src/test/kotlin/function/LispFunctionTest.kt b/src/test/kotlin/function/LispFunctionTest.kt index 975f140..8c16459 100644 --- a/src/test/kotlin/function/LispFunctionTest.kt +++ b/src/test/kotlin/function/LispFunctionTest.kt @@ -2,9 +2,12 @@ package function import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import sexpression.Cons import sexpression.Nil +@TestInstance(PER_CLASS) class LispFunctionTest { @Test diff --git a/src/test/kotlin/function/LispSpecialFunctionTest.kt b/src/test/kotlin/function/LispSpecialFunctionTest.kt index 6ee4c77..86c5bfe 100644 --- a/src/test/kotlin/function/LispSpecialFunctionTest.kt +++ b/src/test/kotlin/function/LispSpecialFunctionTest.kt @@ -2,9 +2,12 @@ package function import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import sexpression.Cons import sexpression.Nil +@TestInstance(PER_CLASS) class LispSpecialFunctionTest { @Test diff --git a/src/test/kotlin/function/UserDefinedFunctionTest.kt b/src/test/kotlin/function/UserDefinedFunctionTest.kt index fd4992f..660bb5c 100644 --- a/src/test/kotlin/function/UserDefinedFunctionTest.kt +++ b/src/test/kotlin/function/UserDefinedFunctionTest.kt @@ -6,6 +6,8 @@ import function.UserDefinedFunction.IllegalKeywordRestPositionException import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import sexpression.Cons import sexpression.LispNumber import sexpression.Nil @@ -13,6 +15,7 @@ import sexpression.Symbol import testutil.TestUtilities.assertIsErrorWithMessage import testutil.TestUtilities.assertSExpressionsMatch +@TestInstance(PER_CLASS) class UserDefinedFunctionTest { companion object { diff --git a/src/test/kotlin/interpreter/LispInterpreterTest.kt b/src/test/kotlin/interpreter/LispInterpreterTest.kt index 96cbef8..fef63e3 100644 --- a/src/test/kotlin/interpreter/LispInterpreterTest.kt +++ b/src/test/kotlin/interpreter/LispInterpreterTest.kt @@ -6,10 +6,13 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import testutil.TestUtilities.createInputStreamFromString import java.io.ByteArrayOutputStream import java.io.PrintStream +@TestInstance(PER_CLASS) class LispInterpreterTest { companion object { diff --git a/src/test/kotlin/recursion/TailCallTest.kt b/src/test/kotlin/recursion/TailCallTest.kt index 7dfdf84..5bc23fd 100644 --- a/src/test/kotlin/recursion/TailCallTest.kt +++ b/src/test/kotlin/recursion/TailCallTest.kt @@ -2,8 +2,11 @@ package recursion import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import recursion.TailCalls.done +@TestInstance(PER_CLASS) class TailCallTest { @Test diff --git a/src/test/kotlin/scanner/LispCommentRemovingInputStreamTest.kt b/src/test/kotlin/scanner/LispCommentRemovingInputStreamTest.kt index 6ef094e..f18f89c 100644 --- a/src/test/kotlin/scanner/LispCommentRemovingInputStreamTest.kt +++ b/src/test/kotlin/scanner/LispCommentRemovingInputStreamTest.kt @@ -4,11 +4,14 @@ import error.Severity.CRITICAL import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import scanner.LispInputStream.MaximumUnreadsExceededException import stream.LispIOException import testutil.TestUtilities.createIOExceptionThrowingInputStream import testutil.TestUtilities.createInputStreamFromString +@TestInstance(PER_CLASS) class LispCommentRemovingInputStreamTest { private fun getLispCommentRemovingInputStreamResult(inputString: String) = diff --git a/src/test/kotlin/scanner/LispScannerLineColumnTest.kt b/src/test/kotlin/scanner/LispScannerLineColumnTest.kt index 770920d..e41c803 100644 --- a/src/test/kotlin/scanner/LispScannerLineColumnTest.kt +++ b/src/test/kotlin/scanner/LispScannerLineColumnTest.kt @@ -3,8 +3,11 @@ package scanner import file.FilePosition import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import testutil.TestUtilities.createInputStreamFromString +@TestInstance(PER_CLASS) class LispScannerLineColumnTest { companion object { diff --git a/src/test/kotlin/scanner/LispScannerTextTest.kt b/src/test/kotlin/scanner/LispScannerTextTest.kt index edd4a6f..8f2dcbb 100644 --- a/src/test/kotlin/scanner/LispScannerTextTest.kt +++ b/src/test/kotlin/scanner/LispScannerTextTest.kt @@ -2,8 +2,11 @@ package scanner import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import testutil.TestUtilities.createInputStreamFromString +@TestInstance(PER_CLASS) class LispScannerTextTest { private fun assertTokenTextMatches(input: String, expectedText: Array) { diff --git a/src/test/kotlin/scanner/LispScannerTypeTest.kt b/src/test/kotlin/scanner/LispScannerTypeTest.kt index a86028b..cbfecc6 100644 --- a/src/test/kotlin/scanner/LispScannerTypeTest.kt +++ b/src/test/kotlin/scanner/LispScannerTypeTest.kt @@ -4,6 +4,8 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import scanner.LispScanner.UnterminatedStringException import testutil.TestUtilities.assertIsErrorWithMessage import testutil.TestUtilities.createInputStreamFromString @@ -20,6 +22,7 @@ import token.RightParenthesis import token.Token import token.TokenFactory.BadCharacterException +@TestInstance(PER_CLASS) class LispScannerTypeTest { private val expectedTypes = mutableListOf>() diff --git a/src/test/kotlin/sexpression/SExpressionTest.kt b/src/test/kotlin/sexpression/SExpressionTest.kt index d24c933..4387442 100644 --- a/src/test/kotlin/sexpression/SExpressionTest.kt +++ b/src/test/kotlin/sexpression/SExpressionTest.kt @@ -4,6 +4,8 @@ import function.UserDefinedFunction import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import sexpression.LispNumber.InvalidNumberException import testutil.TestUtilities.assertIsErrorWithMessage import testutil.TestUtilities.assertSExpressionsMatch @@ -11,6 +13,7 @@ import testutil.TestUtilities.makeList import java.math.BigInteger import java.util.Locale +@TestInstance(PER_CLASS) class SExpressionTest { private fun assertSExpressionMatchesString(expected: String, sExpression: SExpression) { diff --git a/src/test/kotlin/stream/SafeInputStreamTest.kt b/src/test/kotlin/stream/SafeInputStreamTest.kt index d27920e..32ab168 100644 --- a/src/test/kotlin/stream/SafeInputStreamTest.kt +++ b/src/test/kotlin/stream/SafeInputStreamTest.kt @@ -4,10 +4,13 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import testutil.TestUtilities.createIOExceptionThrowingInputStream import testutil.TestUtilities.createInputStreamFromString import util.Characters.EOF +@TestInstance(PER_CLASS) class SafeInputStreamTest { private lateinit var safe: SafeInputStream diff --git a/src/test/kotlin/stream/SafeOutputStreamTest.kt b/src/test/kotlin/stream/SafeOutputStreamTest.kt index 1f840da..af69d46 100644 --- a/src/test/kotlin/stream/SafeOutputStreamTest.kt +++ b/src/test/kotlin/stream/SafeOutputStreamTest.kt @@ -4,9 +4,12 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import testutil.TestUtilities.createIOExceptionThrowingOutputStream import java.io.ByteArrayOutputStream +@TestInstance(PER_CLASS) class SafeOutputStreamTest { private lateinit var safe: SafeOutputStream diff --git a/src/test/kotlin/terminal/ControlSequenceHandlerTest.kt b/src/test/kotlin/terminal/ControlSequenceHandlerTest.kt index f8b043d..298f3a9 100644 --- a/src/test/kotlin/terminal/ControlSequenceHandlerTest.kt +++ b/src/test/kotlin/terminal/ControlSequenceHandlerTest.kt @@ -3,6 +3,8 @@ package terminal import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import stream.SafeInputStream import terminal.ControlSequence.NullControlSequence import terminal.ControlSequenceHandler.Companion.isEscape @@ -14,6 +16,7 @@ import terminal.SelectGraphicRendition.YELLOW import testutil.TestUtilities import util.Characters.EOF +@TestInstance(PER_CLASS) class ControlSequenceHandlerTest { private lateinit var handler: ControlSequenceHandler diff --git a/src/test/kotlin/terminal/ControlSequenceTest.kt b/src/test/kotlin/terminal/ControlSequenceTest.kt index 0b39135..4d24a6d 100644 --- a/src/test/kotlin/terminal/ControlSequenceTest.kt +++ b/src/test/kotlin/terminal/ControlSequenceTest.kt @@ -6,6 +6,8 @@ import com.googlecode.lanterna.terminal.virtual.VirtualTerminal import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import terminal.ControlSequence.NullControlSequence import terminal.SelectGraphicRendition.GREEN import terminal.SelectGraphicRendition.PURPLE @@ -14,6 +16,7 @@ import terminal.SelectGraphicRendition.RESET import terminal.SelectGraphicRendition.YELLOW import java.util.HashSet +@TestInstance(PER_CLASS) class ControlSequenceTest { private lateinit var indicatorSet: MutableSet diff --git a/src/test/kotlin/terminal/LispTerminalTest.kt b/src/test/kotlin/terminal/LispTerminalTest.kt index d4629ee..60756e5 100644 --- a/src/test/kotlin/terminal/LispTerminalTest.kt +++ b/src/test/kotlin/terminal/LispTerminalTest.kt @@ -11,8 +11,11 @@ import com.googlecode.lanterna.input.KeyType.Escape import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import terminal.LispTerminal.END_OF_SEGMENT +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS +import terminal.LispTerminal.Companion.END_OF_SEGMENT +@TestInstance(PER_CLASS) class LispTerminalTest { private lateinit var terminal: VirtualTerminalInteractor diff --git a/src/test/kotlin/terminal/TerminalConfigurationTest.kt b/src/test/kotlin/terminal/TerminalConfigurationTest.kt index a73e085..bc5a6a6 100644 --- a/src/test/kotlin/terminal/TerminalConfigurationTest.kt +++ b/src/test/kotlin/terminal/TerminalConfigurationTest.kt @@ -3,11 +3,14 @@ package terminal import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import stream.LispIOException import java.io.IOException import java.io.PipedInputStream import java.io.PipedOutputStream +@TestInstance(PER_CLASS) class TerminalConfigurationTest { private lateinit var configuration: TerminalConfiguration diff --git a/src/test/kotlin/terminal/TerminalHistoryTest.kt b/src/test/kotlin/terminal/TerminalHistoryTest.kt index 3e84d43..fca32ea 100644 --- a/src/test/kotlin/terminal/TerminalHistoryTest.kt +++ b/src/test/kotlin/terminal/TerminalHistoryTest.kt @@ -4,6 +4,10 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS + +@TestInstance(PER_CLASS) class TerminalHistoryTest { private lateinit var history: TerminalHistory diff --git a/src/test/kotlin/terminal/VirtualTerminalInteractor.kt b/src/test/kotlin/terminal/VirtualTerminalInteractor.kt index b0596b6..6e77941 100644 --- a/src/test/kotlin/terminal/VirtualTerminalInteractor.kt +++ b/src/test/kotlin/terminal/VirtualTerminalInteractor.kt @@ -8,7 +8,7 @@ import com.googlecode.lanterna.terminal.virtual.DefaultVirtualTerminal import com.googlecode.lanterna.terminal.virtual.VirtualTerminal import org.assertj.core.api.Assertions.assertThat import org.junit.Assert.fail -import terminal.LispTerminal.END_OF_SEGMENT +import terminal.LispTerminal.Companion.END_OF_SEGMENT import java.io.IOException import java.io.PipedInputStream import java.io.PipedOutputStream