aboutsummaryrefslogtreecommitdiff
path: root/core/src/test/java/coffee/liz/abstractionengine
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/test/java/coffee/liz/abstractionengine')
-rw-r--r--core/src/test/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngineTest.java578
1 files changed, 578 insertions, 0 deletions
diff --git a/core/src/test/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngineTest.java b/core/src/test/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngineTest.java
new file mode 100644
index 0000000..31d823a
--- /dev/null
+++ b/core/src/test/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngineTest.java
@@ -0,0 +1,578 @@
+package coffee.liz.abstractionengine.app.ui.editor.vim;
+
+import coffee.liz.abstractionengine.app.ui.editor.CursorStyle;
+import coffee.liz.abstractionengine.app.ui.editor.KeyEvent;
+import coffee.liz.abstractionengine.app.ui.editor.LineNumberMode;
+import coffee.liz.abstractionengine.app.ui.editor.EditorModifier;
+import com.badlogic.gdx.Input;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class VimEngineTest {
+ private static class FakeBuffer implements EditorBuffer {
+ private String text;
+ private int cursor;
+ private int selectionStart;
+ private int selectionEnd;
+ private boolean hasSelection;
+
+ private FakeBuffer(final String text, final int cursor) {
+ this.text = text;
+ this.cursor = Math.max(0, Math.min(cursor, text.length()));
+ this.selectionStart = 0;
+ this.selectionEnd = 0;
+ this.hasSelection = false;
+ }
+
+ @Override
+ public String getText() {
+ return text;
+ }
+
+ @Override
+ public void setText(final String text) {
+ this.text = text;
+ this.cursor = Math.max(0, Math.min(cursor, text.length()));
+ if (hasSelection) {
+ this.selectionStart = Math.max(0, Math.min(selectionStart, text.length()));
+ this.selectionEnd = Math.max(0, Math.min(selectionEnd, text.length()));
+ this.hasSelection = selectionStart != selectionEnd;
+ }
+ }
+
+ @Override
+ public int getCursorPosition() {
+ return cursor;
+ }
+
+ @Override
+ public void setCursorPosition(final int cursorPosition) {
+ this.cursor = Math.max(0, Math.min(cursorPosition, text.length()));
+ }
+
+ @Override
+ public int getCursorLine() {
+ int line = 0;
+ for (int i = 0; i < Math.min(cursor, text.length()); i++) {
+ if (text.charAt(i) == '\n') {
+ line++;
+ }
+ }
+ return line;
+ }
+
+ @Override
+ public int getLines() {
+ if (text.isEmpty()) {
+ return 1;
+ }
+ int lines = 1;
+ for (int i = 0; i < text.length(); i++) {
+ if (text.charAt(i) == '\n') {
+ lines++;
+ }
+ }
+ return lines;
+ }
+
+ @Override
+ public void moveCursorLine(final int line) {
+ final int targetLine = Math.max(0, Math.min(line, getLines() - 1));
+ final int currentLineStart = findLineStartByNumber(getCursorLine());
+ final int currentColumn = cursor - currentLineStart;
+ final int targetStart = findLineStartByNumber(targetLine);
+ final int targetEndExclusive = findLineEndExclusive(targetStart);
+ this.cursor = Math.min(targetStart + currentColumn, targetEndExclusive);
+ }
+
+ @Override
+ public void setSelectionEndpoints(final int selectionStart, final int cursorPosition) {
+ final int clampedSelectionStart = Math.max(0, Math.min(selectionStart, text.length()));
+ this.cursor = Math.max(0, Math.min(cursorPosition, text.length()));
+ this.selectionStart = Math.min(clampedSelectionStart, this.cursor);
+ this.selectionEnd = Math.max(clampedSelectionStart, this.cursor);
+ this.hasSelection = this.selectionStart != this.selectionEnd;
+ }
+
+ @Override
+ public void clearSelection() {
+ this.hasSelection = false;
+ this.selectionStart = 0;
+ this.selectionEnd = 0;
+ }
+
+ private int findLineStartByNumber(final int targetLine) {
+ int line = 0;
+ int index = 0;
+ while (line < targetLine && index < text.length()) {
+ if (text.charAt(index) == '\n') {
+ line++;
+ }
+ index++;
+ }
+ return index;
+ }
+
+ private int findLineEndExclusive(final int lineStart) {
+ int index = lineStart;
+ while (index < text.length() && text.charAt(index) != '\n') {
+ index++;
+ }
+ return index;
+ }
+ }
+
+ @Test
+ public void visualSelectionAndEscapeRestoresCursorToSelectionStart() {
+ final FakeBuffer buffer = new FakeBuffer("abcdef\n", 1);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'v', 'l', 'l');
+
+ assertEquals(1, buffer.selectionStart);
+ assertEquals(3, buffer.selectionEnd);
+ assertEquals(3, buffer.getCursorPosition());
+
+ controller.handleKeyDown(Input.Keys.ESCAPE);
+
+ assertFalse(buffer.hasSelection);
+ assertEquals(1, buffer.getCursorPosition());
+ }
+
+ @Test
+ public void normalModeUsesBlockCursorAndRelativeLines() {
+ final FakeBuffer buffer = new FakeBuffer("abc", 0);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ assertEquals(CursorStyle.BLOCK, controller.getRenderState().cursorStyle());
+ assertEquals(LineNumberMode.RELATIVE, controller.getRenderState().lineNumberMode());
+ }
+
+ @Test
+ public void insertModeUsesCaretAndAbsoluteLines() {
+ final FakeBuffer buffer = new FakeBuffer("abc", 0);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'i');
+
+ assertEquals(CursorStyle.CARET, controller.getRenderState().cursorStyle());
+ assertEquals(LineNumberMode.ABSOLUTE, controller.getRenderState().lineNumberMode());
+ }
+
+ @Test
+ public void enteringVisualModeDoesNotMoveCursor() {
+ final FakeBuffer buffer = new FakeBuffer("abcdef", 2);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ controller.handleKeyTyped('v');
+
+ assertEquals(2, buffer.getCursorPosition());
+ }
+
+ @Test
+ public void visualModeCanMoveLeftToShrinkSelection() {
+ final FakeBuffer buffer = new FakeBuffer("abcdef", 1);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'v', 'l', 'l');
+ assertEquals(1, buffer.selectionStart);
+ assertEquals(3, buffer.selectionEnd);
+
+ type(controller, 'h');
+ assertEquals(1, buffer.selectionStart);
+ assertEquals(2, buffer.selectionEnd);
+ assertEquals(2, buffer.getCursorPosition());
+ }
+
+ @Test
+ public void visualBackwardSelectionIncludesAnchorCharacter() {
+ final FakeBuffer buffer = new FakeBuffer("abcdef", 3);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'v', 'h');
+
+ assertTrue(buffer.hasSelection);
+ assertEquals("cd", buffer.getText().substring(buffer.selectionStart, buffer.selectionEnd));
+ }
+
+ @Test
+ public void visualZeroMotionBackwardIncludesAnchorCharacter() {
+ final FakeBuffer buffer = new FakeBuffer("abcdef", 3);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'v', '0');
+
+ assertTrue(buffer.hasSelection);
+ assertEquals("abcd", buffer.getText().substring(buffer.selectionStart, buffer.selectionEnd));
+ }
+
+ @Test
+ public void visualModeGgMovesToStartAndKeepsSelection() {
+ final FakeBuffer buffer = new FakeBuffer("one\ntwo\nthree", 7);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'v', 'g', 'g');
+
+ assertEquals(0, buffer.getCursorPosition());
+ assertTrue(buffer.hasSelection);
+ assertEquals("one\ntwo\n", buffer.getText().substring(buffer.selectionStart, buffer.selectionEnd));
+ }
+
+ @Test
+ public void visualLineSelectionWorksWithShiftV() {
+ final FakeBuffer buffer = new FakeBuffer("one\ntwo\nthree\n", 0);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'V', 'j');
+
+ assertTrue(buffer.hasSelection);
+ assertEquals(0, buffer.selectionStart);
+ assertEquals(8, buffer.selectionEnd);
+ }
+
+ @Test
+ public void visualLineJThenKUpdatesSelectionAndCursorLine() {
+ final FakeBuffer buffer = new FakeBuffer("one\ntwo\nthree\n", 0);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'V', 'j');
+ assertEquals(0, buffer.selectionStart);
+ assertEquals(8, buffer.selectionEnd);
+
+ type(controller, 'k');
+ assertEquals(0, buffer.selectionStart);
+ assertEquals(4, buffer.selectionEnd);
+ assertEquals(0, buffer.getCursorLine());
+ }
+
+ @Test
+ public void visualLineModeDoesNotMoveCursorPastLastRealTrailingEmptyLine() {
+ final FakeBuffer buffer = new FakeBuffer("a\n\n\n", 2);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'V', 'j');
+
+ assertEquals(3, buffer.getCursorPosition());
+ assertEquals(2, buffer.getCursorLine());
+ }
+
+ @Test
+ public void openLineBelowPlacesCursorOnNewLine() {
+ final FakeBuffer buffer = new FakeBuffer("abc\ndef", 1);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ controller.handleKeyTyped('o');
+
+ assertEquals("abc\n\ndef", buffer.getText());
+ assertEquals(4, buffer.getCursorPosition());
+ }
+
+ @Test
+ public void moveDownCanReachTrailingEmptyLines() {
+ final FakeBuffer buffer = new FakeBuffer("a\n\n\n", 0);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'j', 'j', 'j');
+
+ assertEquals(3, buffer.getCursorPosition());
+ }
+
+ @Test
+ public void openLineBelowOnTrailingEmptyLineMovesOneLineDown() {
+ final FakeBuffer buffer = new FakeBuffer("a\n\n\n", 2);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ controller.handleKeyTyped('o');
+
+ assertEquals("a\n\n\n\n", buffer.getText());
+ assertEquals(3, buffer.getCursorPosition());
+ }
+
+ @Test
+ public void gMovesToLastNavigableCharacter() {
+ final FakeBuffer buffer = new FakeBuffer("alpha\nbeta\n", 0);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ controller.handleKeyTyped('G');
+
+ assertEquals(9, buffer.getCursorPosition());
+ }
+
+ @Test
+ public void ggMovesToTopOfFile() {
+ final FakeBuffer buffer = new FakeBuffer("alpha\nbeta\n", 9);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'g', 'g');
+
+ assertEquals(0, buffer.getCursorPosition());
+ }
+
+ @Test
+ public void ddThenPastePlacesLineBelow() {
+ final FakeBuffer buffer = new FakeBuffer("a\nb", 2);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'd', 'd', 'p');
+
+ assertEquals("a\nb\n", buffer.getText());
+ }
+
+ @Test
+ public void yyThenPastePlacesLineBelow() {
+ final FakeBuffer buffer = new FakeBuffer("one\ntwo\n", 0);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'y', 'y', 'p');
+
+ assertEquals("one\none\ntwo\n", buffer.getText());
+ }
+
+ @Test
+ public void dwDeletesWord() {
+ final FakeBuffer buffer = new FakeBuffer("one two", 0);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'd', 'w');
+
+ assertEquals("two", buffer.getText());
+ }
+
+ @Test
+ public void dlDeletesExactlyOneCharacter() {
+ final FakeBuffer buffer = new FakeBuffer("abcd", 1);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'd', 'l');
+
+ assertEquals("acd", buffer.getText());
+ }
+
+ @Test
+ public void dlOnLastCharacterDeletesLastCharacter() {
+ final FakeBuffer buffer = new FakeBuffer("abcd", 3);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'd', 'l');
+
+ assertEquals("abc", buffer.getText());
+ assertEquals(2, buffer.getCursorPosition());
+ }
+
+ @Test
+ public void undoRevertsLatestEdit() {
+ final FakeBuffer buffer = new FakeBuffer("abc", 0);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ controller.handleKeyTyped('x');
+ assertEquals("bc", buffer.getText());
+
+ controller.handleKeyTyped('u');
+ assertEquals("abc", buffer.getText());
+ assertEquals(0, buffer.getCursorPosition());
+ }
+
+ @Test
+ public void ctrlRRedoesAfterUndo() {
+ final FakeBuffer buffer = new FakeBuffer("abc", 0);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ controller.handleKeyTyped('x');
+ controller.handleKeyTyped('u');
+ controller.handleKeyDown(Input.Keys.R, true);
+
+ assertEquals("bc", buffer.getText());
+ }
+
+ @Test
+ public void keyDownMotionReturnsSuppressTypedAndMovesCursor() {
+ final FakeBuffer buffer = new FakeBuffer("abc\ndef", 1);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ final boolean handled = controller.onKeyDown(new KeyEvent(Input.Keys.J, Set.of()));
+
+ assertTrue(handled);
+ assertEquals(5, buffer.getCursorPosition());
+ }
+
+ @Test
+ public void keyDownShiftVStartsVisualLineMode() {
+ final FakeBuffer buffer = new FakeBuffer("one\ntwo\nthree\n", 0);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ final boolean handled = controller.onKeyDown(new KeyEvent(Input.Keys.V, Set.of(EditorModifier.SHIFT)));
+
+ assertTrue(handled);
+ assertEquals("-- VISUAL LINE --", controller.getStatusText());
+ }
+
+ @Test
+ public void controlCharacterRRedoesAfterUndo() {
+ final FakeBuffer buffer = new FakeBuffer("abc", 0);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ controller.handleKeyTyped('x');
+ controller.handleKeyTyped('u');
+ controller.handleKeyTyped((char) 18);
+
+ assertEquals("bc", buffer.getText());
+ }
+
+ @Test
+ public void searchAndNextFindMatches() {
+ final FakeBuffer buffer = new FakeBuffer("cat dog cat", 0);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ controller.handleKeyTyped('/');
+ controller.handleKeyTyped('c');
+ controller.handleKeyTyped('a');
+ controller.handleKeyTyped('t');
+ controller.handleKeyTyped('\n');
+
+ assertEquals(8, buffer.getCursorPosition());
+
+ controller.handleKeyTyped('n');
+ assertEquals(0, buffer.getCursorPosition());
+ }
+
+ @Test
+ public void countMotionAdvancesMultipleCharacters() {
+ final FakeBuffer buffer = new FakeBuffer("abcdef", 0);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, '3', 'l');
+
+ assertEquals(3, buffer.getCursorPosition());
+ }
+
+ @Test
+ public void bStopsAtPunctuationWordBeforeSymbols() {
+ final FakeBuffer buffer = new FakeBuffer("let lambda = ", 12);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'b');
+
+ assertEquals(11, buffer.getCursorPosition());
+ }
+
+ @Test
+ public void wStopsAtPunctuationWordBeforeSymbols() {
+ final FakeBuffer buffer = new FakeBuffer("let lambda = ", 4);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'w');
+
+ assertEquals(11, buffer.getCursorPosition());
+ }
+
+ @Test
+ public void secondWFromPunctuationMovesToNextIdentifier() {
+ final FakeBuffer buffer = new FakeBuffer("let lambda = x", 4);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'w', 'w');
+
+ assertEquals(13, buffer.getCursorPosition());
+ }
+
+ @Test
+ public void secondBFromPunctuationMovesToPreviousIdentifier() {
+ final FakeBuffer buffer = new FakeBuffer("let lambda = ", 12);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'b', 'b');
+
+ assertEquals(4, buffer.getCursorPosition());
+ }
+
+ @Test
+ public void appendMovesPastCurrentCharacter() {
+ final FakeBuffer buffer = new FakeBuffer("abc", 1);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ controller.handleKeyTyped('a');
+ insertCharacter(buffer, 'X');
+ controller.handleKeyDown(Input.Keys.ESCAPE);
+
+ assertEquals("abXc", buffer.getText());
+ }
+
+ @Test
+ public void appendAtEndOfLineInsertsAfterLastCharacter() {
+ final FakeBuffer buffer = new FakeBuffer("abc", 2);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ controller.handleKeyTyped('a');
+ insertCharacter(buffer, 'X');
+ controller.handleKeyDown(Input.Keys.ESCAPE);
+
+ assertEquals("abcX", buffer.getText());
+ }
+
+ @Test
+ public void verticalMoveUsesCurrentColumnNotStaleColumn() {
+ final FakeBuffer buffer = new FakeBuffer("12345678\n12\n12345678\n", 6);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'j');
+ assertEquals(10, buffer.getCursorPosition());
+
+ type(controller, 'h');
+ assertEquals(9, buffer.getCursorPosition());
+
+ type(controller, 'k');
+ assertEquals(0, buffer.getCursorPosition());
+ }
+
+ private static void type(final VimEngine controller, final char... chars) {
+ for (final char character : chars) {
+ controller.handleKeyTyped(character);
+ }
+ }
+
+ private static void insertCharacter(final FakeBuffer buffer, final char character) {
+ final int cursor = buffer.getCursorPosition();
+ final String text = buffer.getText();
+ buffer.setText(text.substring(0, cursor) + character + text.substring(cursor));
+ buffer.setCursorPosition(cursor + 1);
+ }
+}