diff options
| author | Elizabeth Hunt <me@liz.coffee> | 2026-02-07 17:22:01 -0800 |
|---|---|---|
| committer | Elizabeth Hunt <me@liz.coffee> | 2026-02-07 17:22:01 -0800 |
| commit | a483f04c16e6b56df24527f6a640e79c86928238 (patch) | |
| tree | 0dc2f149629367150e606b6b86f767865ac627e5 /core/src/test/java/coffee | |
| parent | 40a7ea05a9d6041842a081bb5a98290a31ea798e (diff) | |
| download | the-abstraction-engine-java-a483f04c16e6b56df24527f6a640e79c86928238.tar.gz the-abstraction-engine-java-a483f04c16e6b56df24527f6a640e79c86928238.zip | |
Major feat: vim
Diffstat (limited to 'core/src/test/java/coffee')
| -rw-r--r-- | core/src/test/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngineTest.java | 578 |
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); + } +} |
