diff --git a/readline/src/main/java/org/aesh/readline/AeshConsoleBuffer.java b/readline/src/main/java/org/aesh/readline/AeshConsoleBuffer.java index adfe013c..2e8bf1e6 100644 --- a/readline/src/main/java/org/aesh/readline/AeshConsoleBuffer.java +++ b/readline/src/main/java/org/aesh/readline/AeshConsoleBuffer.java @@ -1,5 +1,7 @@ package org.aesh.readline; +import org.aesh.readline.cursor.Line; +import org.aesh.readline.cursor.CursorListener; import org.aesh.readline.history.InMemoryHistory; import org.aesh.readline.paste.PasteManager; import org.aesh.readline.undo.UndoAction; @@ -34,12 +36,13 @@ public class AeshConsoleBuffer implements ConsoleBuffer { private final boolean ansiMode; private static final Logger LOGGER = LoggerUtil.getLogger(AeshConsoleBuffer.class.getName()); + private final CursorListener cursorListener; public AeshConsoleBuffer(Connection connection, Prompt prompt, EditMode editMode, History history, CompletionHandler completionHandler, Size size, - boolean ansi) { + boolean ansi, CursorListener listener) { this.connection = connection; this.ansiMode = ansi; this.buffer = new Buffer(prompt); @@ -58,6 +61,7 @@ public AeshConsoleBuffer(Connection connection, Prompt prompt, this.size = size; this.editMode = editMode; + this.cursorListener = listener; } @Override public History history() { @@ -104,6 +108,9 @@ public PasteManager pasteManager() { public void moveCursor(int where) { buffer.move(connection.stdoutHandler(), where, size().getWidth(), isViMode()); + if (cursorListener != null) { + cursorListener.moved(new Line(buffer, connection, size.getWidth())); + } } @Override diff --git a/readline/src/main/java/org/aesh/readline/Buffer.java b/readline/src/main/java/org/aesh/readline/Buffer.java index 3d24f023..7e7a0bf5 100644 --- a/readline/src/main/java/org/aesh/readline/Buffer.java +++ b/readline/src/main/java/org/aesh/readline/Buffer.java @@ -29,6 +29,7 @@ import java.util.Arrays; import java.util.function.Consumer; import java.util.logging.Logger; +import org.aesh.readline.cursor.CursorLocator; /** * Buffer to keep track of text and cursor position in the console. @@ -54,10 +55,12 @@ public class Buffer { private boolean isPromptDisplayed = false; private boolean deletingBackward = true; + private final CursorLocator locator; Buffer() { line = new int[1024]; prompt = new Prompt(""); + locator = new CursorLocator(this); } Buffer(Prompt prompt) { @@ -66,6 +69,7 @@ public class Buffer { this.prompt = prompt; else this.prompt = new Prompt(""); + locator = new CursorLocator(this); } public Buffer(Buffer buf) { @@ -73,6 +77,11 @@ public Buffer(Buffer buf) { cursor = buf.cursor; size = buf.size; prompt = buf.prompt.copy(); + locator = new CursorLocator(this); + } + + public CursorLocator getCursorLocator() { + return locator; } public int get(int pos) { @@ -113,6 +122,7 @@ public void reset() { isPromptDisplayed = false; if(multiLine) multiLineBuffer = new int[0]; + locator.clear(); } public void setIsPromptDisplayed(boolean isPromptDisplayed) { @@ -160,16 +170,31 @@ public void setMultiLine(boolean multi) { multiLine = multi; } + /** + * Some completion occured, do not try to compute character index location. + * This could be revisited to implement a strategy. + */ + public void invalidateCursorLocation() { + if (isMultiLine()) { + locator.invalidateCursorLocation(); + } + } + public void updateMultiLineBuffer() { int originalSize = multiLineBuffer.length; - if(lineEndsWithBackslash()) { + // Store the size of each line. + int cmdSize; + if (lineEndsWithBackslash()) { + cmdSize = size - 1; multiLineBuffer = Arrays.copyOf(multiLineBuffer, originalSize + size-1); System.arraycopy(line, 0, multiLineBuffer, originalSize, size-1); } else { + cmdSize = size; multiLineBuffer = Arrays.copyOf(multiLineBuffer, originalSize + size); System.arraycopy(line, 0, multiLineBuffer, originalSize, size); } + locator.addLine(cmdSize, prompt.getLength()); clear(); prompt = new Prompt("> "); cursor = 0; @@ -341,7 +366,7 @@ private int[] syncCursorWhenBufferIsAtTerminalEdge(int currentPos, int newPos, i return builder.toArray(); } - private int[] moveNumberOfColumns(int column, char direction) { + public int[] moveNumberOfColumns(int column, char direction) { if(column < 10) { int[] out = new int[4]; out[0] = 27; // esc @@ -732,7 +757,7 @@ public void delete(Consumer out, int delta, int width, boolean viMode) { deletingBackward = false; } else if (delta < 0) { - delta = - Math.min(- delta, cursor); + delta = -Math.min(-delta, cursor); System.arraycopy(line, cursor, line, cursor + delta, size - cursor); size += delta; cursor += delta; @@ -740,6 +765,9 @@ else if (delta < 0) { deletingBackward = true; } + // Erase the remaining. + Arrays.fill(line, size, line.length, 0); + if(viMode) { //if(!deletingBackward) // cursor--; diff --git a/readline/src/main/java/org/aesh/readline/Readline.java b/readline/src/main/java/org/aesh/readline/Readline.java index 919b7a28..4354be5b 100644 --- a/readline/src/main/java/org/aesh/readline/Readline.java +++ b/readline/src/main/java/org/aesh/readline/Readline.java @@ -19,6 +19,7 @@ */ package org.aesh.readline; +import org.aesh.readline.cursor.CursorListener; import org.aesh.readline.action.Action; import org.aesh.readline.action.ActionDecoder; import org.aesh.readline.action.KeyAction; @@ -118,18 +119,18 @@ public void readline(Connection conn, Prompt prompt, Consumer requestHan public void readline(Connection conn, Prompt prompt, Consumer requestHandler, List completions, List>> preProcessors ) { - readline(conn, prompt, requestHandler, completions, preProcessors, null); + readline(conn, prompt, requestHandler, completions, preProcessors, null, null); } public void readline(Connection conn, Prompt prompt, Consumer requestHandler, List completions, List>> preProcessors, - History history) { + History history, CursorListener listener) { synchronized(this) { if (inputProcessor != null) { throw new IllegalStateException("Already reading a line"); } - inputProcessor = new AeshInputProcessor(conn, prompt, requestHandler, completions, preProcessors, history); + inputProcessor = new AeshInputProcessor(conn, prompt, requestHandler, completions, preProcessors, history, listener); } inputProcessor.start(); processInput(); @@ -170,7 +171,7 @@ private AeshInputProcessor( Consumer requestHandler, List completions, List>> preProcessors, - History newHistory) { + History newHistory, CursorListener listener) { completionHandler.clear(); completionHandler.addCompletions(completions); @@ -178,7 +179,7 @@ private AeshInputProcessor( new AeshConsoleBuffer(conn, prompt, editMode, //use newHistory if its not null newHistory != null ? newHistory : history, - completionHandler, size, true); + completionHandler, size, true, listener); this.conn = conn; this.requestHandler = requestHandler; diff --git a/readline/src/main/java/org/aesh/readline/completion/CompletionHandler.java b/readline/src/main/java/org/aesh/readline/completion/CompletionHandler.java index d56c4c55..44656d13 100644 --- a/readline/src/main/java/org/aesh/readline/completion/CompletionHandler.java +++ b/readline/src/main/java/org/aesh/readline/completion/CompletionHandler.java @@ -215,6 +215,7 @@ private void displayCompletions(List completions, Buffer buffer, inputProcessor.buffer().size().getHeight(), inputProcessor.buffer().size().getWidth())); buffer.setIsPromptDisplayed(false); + buffer.invalidateCursorLocation(); inputProcessor.buffer().drawLine(); } diff --git a/readline/src/main/java/org/aesh/readline/cursor/CursorListener.java b/readline/src/main/java/org/aesh/readline/cursor/CursorListener.java new file mode 100644 index 00000000..54089fd4 --- /dev/null +++ b/readline/src/main/java/org/aesh/readline/cursor/CursorListener.java @@ -0,0 +1,28 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.aesh.readline.cursor; + +/** + * + * @author jdenise@redhat.com + */ +public interface CursorListener { + void moved(Line line); +} diff --git a/readline/src/main/java/org/aesh/readline/cursor/CursorLocation.java b/readline/src/main/java/org/aesh/readline/cursor/CursorLocation.java new file mode 100644 index 00000000..09564613 --- /dev/null +++ b/readline/src/main/java/org/aesh/readline/cursor/CursorLocation.java @@ -0,0 +1,49 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.aesh.readline.cursor; + +/** + * + * @author jdenise@redhat.com + */ +public class CursorLocation { + + private final int column; + private final int row; + + public CursorLocation(int row, int column) { + this.row = row; + this.column = column; + } + + /** + * @return the column + */ + public int getColumn() { + return column; + } + + /** + * @return the row + */ + public int getRow() { + return row; + } +} diff --git a/readline/src/main/java/org/aesh/readline/cursor/CursorLocator.java b/readline/src/main/java/org/aesh/readline/cursor/CursorLocator.java new file mode 100644 index 00000000..e891d13c --- /dev/null +++ b/readline/src/main/java/org/aesh/readline/cursor/CursorLocator.java @@ -0,0 +1,105 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.aesh.readline.cursor; + +import java.util.ArrayList; +import java.util.List; +import org.aesh.readline.Buffer; + +/** + * Map a command character index onto a cursor COL/ROW. + * + * @author jdenise@redhat.com + */ +public class CursorLocator { + + private final List linesSize = new ArrayList<>(); + private boolean invalidatedLines; + + private final Buffer buffer; + + public CursorLocator(Buffer buffer) { + this.buffer = buffer; + } + + public void addLine(int size, int promptSize) { + linesSize.add(size); + linesSize.add(promptSize); + } + + public boolean isLocationInvalidated() { + return invalidatedLines; + } + + public void invalidateCursorLocation() { + invalidatedLines = true; + } + + /** + * The core logic of the locator. Map a command index onto an absolute + * COL/ROW cursor location. + * + * @param index The commnd index. + * @param width The terminal width. + * @return + */ + public CursorLocation locate(int index, int width) { + // Upper lines location has been lost. + if (isLocationInvalidated()) { + return null; + } + int cumulated = 0; + + List allLines = new ArrayList<>(); + allLines.addAll(linesSize); + allLines.add(buffer.length()); + allLines.add(buffer.prompt().getLength()); + int lineIndex = 0; + for (int i = 0; i < allLines.size(); i++) { + int cmdSize = allLines.get(i++); + int promptSize = allLines.get(i); + lineIndex += 1; + if (cumulated + cmdSize > index) { + int part = index - cumulated; + int col = (part + promptSize) % width; + // if the part + prompt is longer than width, then + // the row is in a lower line. + lineIndex += (promptSize + part) / width; + return new CursorLocation(lineIndex - 1, col); + } + cumulated += cmdSize; + // Each line could be wrapped if longer than width. + lineIndex += (cmdSize + promptSize) / width; + } + // we are on the last line at the last character. + if (cumulated == index) { + int cmdSize = allLines.get(allLines.size() - 2); + int promptSize = allLines.get(allLines.size() - 1); + int col = (cmdSize + promptSize) % width; + return new CursorLocation(lineIndex - 1, col); + } else { + return null; + } + } + + public void clear() { + linesSize.clear(); + } +} diff --git a/readline/src/main/java/org/aesh/readline/cursor/Line.java b/readline/src/main/java/org/aesh/readline/cursor/Line.java new file mode 100644 index 00000000..973e3040 --- /dev/null +++ b/readline/src/main/java/org/aesh/readline/cursor/Line.java @@ -0,0 +1,345 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.aesh.readline.cursor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; +import org.aesh.readline.Buffer; +import org.aesh.readline.terminal.formatting.Color; +import org.aesh.readline.terminal.formatting.TerminalCharacter; +import org.aesh.readline.terminal.formatting.TerminalColor; +import org.aesh.terminal.Connection; +import org.aesh.util.ANSI; +import org.aesh.util.Parser; + +/** + * A command line. This line abstract commands spread-out on multiple lines. + * + * @author jdenise@redhat.com + */ +public class Line { + + private static final Logger LOG = Logger.getLogger(Line.class.getName()); + + /** + * A CursorAction is an action that modifies the cursor. + */ + public abstract class CursorAction { + + public abstract void apply(); + } + + /** + * Move the cursor to a given index. It will take into account multilines to + * retrieve the column/row of a given index. + */ + public class MoveAction extends CursorAction { + + //private CursorTransaction transaction; + private final int index; + + MoveAction(int index) { + this.index = index; + } + + @Override + public void apply() { + CursorLocation loc = buffer.getCursorLocator().locate(index, width); + if (loc == null) { + throw new RuntimeException("Null Location for " + index); + } + CursorLocation cursorLoc = buffer.getCursorLocator().locate(buffer.multiCursor(), width); + moveUp(cursorLoc.getRow() - loc.getRow()); + moveBackward(cursorLoc.getColumn()); + moveForward(loc.getColumn()); + } + } + + /** + * Move to the index and Colorize the character. + */ + public class ColorizeAction extends CursorAction { + + private final Color text; + private final Color background; + private final int index; + private final boolean bright; + + ColorizeAction(int index, Color text, Color background, boolean bright) { + this.index = index; + this.text = text; + this.background = background; + this.bright = bright; + } + + @Override + public void apply() { + MoveAction move = new MoveAction(index); + move.apply(); + char c = buffer.asString().charAt(index); + // Usage of Intensity.BRIGHT breaks coloring on Windows. + // Bold works on both windows and MacOSX, keeping BOLD for all for now. + //if (OSUtils.IS_WINDOWS) { + TerminalCharacter characterData = new TerminalCharacter(c, new TerminalColor(text, background)); + if (bright) { + connection.stdoutHandler().accept(Parser.toCodePoints(ANSI.BOLD)); + } else { + connection.stdoutHandler().accept(Parser.toCodePoints(ANSI.BOLD_OFF)); + } + connection.stdoutHandler().accept(Parser.toCodePoints(characterData.toString())); + //} else { + // TerminalColor color = bright ? new TerminalColor(text, background, Color.Intensity.BRIGHT) : new TerminalColor(text, background); + // TerminalCharacter characterData = new TerminalCharacter(c, color); + // connection.stdoutHandler().accept(Parser.toCodePoints(characterData.toString())); + //} + moveBackward(1); + } + } + + /** + * Move cursor backward. + */ + public class MoveBackwardAction extends CursorAction { + + int move; + + MoveBackwardAction(int move) { + this.move = move; + } + + @Override + public void apply() { + moveBackward(move); + } + } + + /** + * Move cursor forward. + */ + public class MoveForwardAction extends CursorAction { + + int move; + + MoveForwardAction(int move) { + this.move = move; + } + + @Override + public void apply() { + moveForward(move); + } + } + + /** + * Move cursor up. + */ + public class MoveUpAction extends CursorAction { + + int move; + + MoveUpAction(int move) { + this.move = move; + } + + @Override + public void apply() { + moveUp(move); + } + } + + /** + * Move cursor down. + */ + public class MoveDownAction extends CursorAction { + + int move; + + MoveDownAction(int move) { + this.move = move; + } + + @Override + public void apply() { + moveDown(move); + } + } + + /** + * A cursor transaction runs action. NB: At the end of a transaction, the + * cursor is back to where it was before the transaction was run. A + * transaction doesn't change the Aesh internal cursor/content states. + */ + public class CursorTransaction { + + private final List actions = new ArrayList<>(); + + CursorTransaction(List actions) { + this.actions.addAll(actions); + } + + public void run() { + saveCursor(); + try { + for (CursorAction a : actions) { + try { + a.apply(); + } catch (Exception ex) { + // Something went wrong, don't go any further + LOG.fine("Exception in Cursor transaction: " + + ex.getLocalizedMessage()); + break; + } + } + } finally { + restoreCursor(); + } + } + } + + /** + * Builder for CursorTransaction. + */ + public class CursorTransactionBuilder { + + private final List actions = new ArrayList<>(); + + public CursorTransactionBuilder move(int value) { + actions.add(new MoveAction(value)); + return this; + } + + public CursorTransactionBuilder colorize(int index, Color text, Color bg, boolean bright) { + actions.add(new ColorizeAction(index, text, bg, bright)); + return this; + } + + public CursorTransactionBuilder moveBackward(int value) { + actions.add(new MoveBackwardAction(value)); + return this; + } + + public CursorTransactionBuilder moveForward(int value) { + actions.add(new MoveForwardAction(value)); + return this; + } + + public CursorTransactionBuilder moveUp(int value) { + actions.add(new MoveUpAction(value)); + return this; + } + + public CursorTransactionBuilder moveDown(int value) { + actions.add(new MoveDownAction(value)); + return this; + } + + public CursorTransaction build() { + if (buffer.getCursorLocator().isLocationInvalidated()) { + return new CursorTransaction(Collections.emptyList()); + } else { + return new CursorTransaction(actions); + } + } + } + + private final Buffer buffer; + private final Connection connection; + private final int width; + + public Line(Buffer buffer, Connection connection, int width) { + this.buffer = buffer; + this.connection = connection; + this.width = width; + } + + /** + * Build a new builder. + * + * @return The builder. + */ + public CursorTransactionBuilder newCursorTransactionBuilder() { + return new CursorTransactionBuilder(); + } + + public CursorLocator getCursorLocator() { + return buffer.getCursorLocator(); + } + + /** + * Gets the index of the character where the cursor is located. + * + * @return The selected character index. + */ + public int getCurrentCharacterIndex() { + return buffer.multiCursor(); + } + + /** + * Returns a string from the beginning of the line to the cursor. Takes into + * account multiple lines. + * + * @return + */ + public String getLineToCursor() { + return buffer.asString().substring(0, buffer.multiCursor()); + } + + /** + * Returns the character located at the cursor. + * + * @return The character. + */ + public int getCharacterAtCursor() { + return buffer.get(buffer.cursor()); + } + + private void moveUp(int delta) { + move(delta, 'A'); + } + + private void moveDown(int delta) { + move(delta, 'B'); + } + + private void moveForward(int delta) { + move(delta, 'C'); + } + + private void moveBackward(int delta) { + move(delta, 'D'); + } + + private void move(int delta, char action) { + if (delta > 0) { + connection.stdoutHandler().accept(buffer.moveNumberOfColumns(delta, action)); + } + } + + private void saveCursor() { + connection.stdoutHandler().accept(Parser.toCodePoints(ANSI.CURSOR_SAVE)); + } + + private void restoreCursor() { + connection.stdoutHandler().accept(Parser.toCodePoints(ANSI.CURSOR_RESTORE)); + } + +} diff --git a/readline/src/test/java/org/aesh/readline/ConsoleBufferTest.java b/readline/src/test/java/org/aesh/readline/ConsoleBufferTest.java index b292ecaa..ea298dd4 100644 --- a/readline/src/test/java/org/aesh/readline/ConsoleBufferTest.java +++ b/readline/src/test/java/org/aesh/readline/ConsoleBufferTest.java @@ -43,12 +43,12 @@ public class ConsoleBufferTest { private ConsoleBuffer createConsoleBuffer(Connection connection) { return new AeshConsoleBuffer(connection, new Prompt("[aesh@rules]: "), EditModeBuilder.builder().create(), - new InMemoryHistory(50), null, connection.size(), true); + new InMemoryHistory(50), null, connection.size(), true, null); } private ConsoleBuffer createConsoleBuffer(Connection connection, String prompt) { return new AeshConsoleBuffer(connection, new Prompt(prompt), EditModeBuilder.builder().create(), - new InMemoryHistory(50), null, connection.size(), true); + new InMemoryHistory(50), null, connection.size(), true, null); } @Test diff --git a/readline/src/test/java/org/aesh/readline/CursorLocatorTest.java b/readline/src/test/java/org/aesh/readline/CursorLocatorTest.java new file mode 100644 index 00000000..9f6e82cd --- /dev/null +++ b/readline/src/test/java/org/aesh/readline/CursorLocatorTest.java @@ -0,0 +1,196 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.aesh.readline; + +import java.io.IOException; +import org.aesh.readline.cursor.CursorLocation; +import org.aesh.readline.cursor.CursorLocator; +import org.aesh.readline.cursor.Line; +import org.aesh.readline.terminal.formatting.Color; +import org.aesh.readline.tty.terminal.TerminalConnection; +import org.junit.Assert; +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +/** + * + * @author jdenise@redhat.com + */ +public class CursorLocatorTest { + + private static final String PROMPT = "test> "; + private static final String MULTI_LINE_PROMPT = "> "; + private static final int WIDTH = 80; + @Test + public void test() { + + { // No cmd + Buffer buffer = new Buffer(new Prompt(PROMPT)); + check(buffer, 0, 0, PROMPT.length(), WIDTH); + } + + { // Index is after buffer of size 0. + Buffer buffer = new Buffer(new Prompt(PROMPT)); + CursorLocator locator = buffer.getCursorLocator(); + CursorLocation loc = locator.locate(10, WIDTH); + assertTrue(loc == null); + } + + { // Nominal, retrieve index after the prompt inside a cmd. + Buffer buffer = new Buffer(new Prompt(PROMPT)); + String cmd = "cmd --opt1"; + int offset = 3; + buffer.insert((c) -> { + }, cmd, WIDTH); + check(buffer, offset, 0, PROMPT.length() + offset, WIDTH); + } + } + + @Test + public void testWrapping() { + + { // Nominal, retrieve index after the prompt inside a cmd longer than + // terminal width. + Buffer buffer = new Buffer(new Prompt(PROMPT)); + String cmd = "cmd --opt1"; + int width = PROMPT.length() + (cmd.length() / 2); + int offset = (cmd.length() / 2); + buffer.insert((c) -> { + }, cmd, width); + check(buffer, offset, 1, 0, width); + checkCursor(buffer, 1, offset, width); + } + + { // Nominal, retrieve index after the prompt inside a cmd longer than + // terminal width. + Buffer buffer = new Buffer(new Prompt(PROMPT)); + String cmd = "cmd --opt1"; + int width = PROMPT.length() + (cmd.length() / 2); + int offset = (cmd.length() / 2) - 1; + buffer.insert((c) -> { + }, cmd, width); + check(buffer, offset, 0, width - 1, width); + } + + { // Set a cmd as large as the width. + Buffer buffer = new Buffer(new Prompt(PROMPT)); + String cmd = "cmd --opt1"; + int width = PROMPT.length() + cmd.length(); + int offset = cmd.length() - 1; + buffer.insert((c) -> { + }, cmd, width); + check(buffer, offset, 0, offset + PROMPT.length(), width); + checkCursor(buffer, 1, 0, width); + } + } + + @Test + public void testMultiline() { + + { + Buffer buffer = new Buffer(new Prompt(PROMPT)); + String cmd = "cmd --opt1\\"; + buffer.insert((c) -> { + }, cmd, WIDTH); + buffer.setMultiLine(true); + buffer.updateMultiLineBuffer(); + String cmd2 = "--opt2"; + buffer.insert((c) -> { + }, cmd2, WIDTH); + int offset = cmd.length() - 1 + cmd2.length(); + check(buffer, offset, 1, cmd2.length() + MULTI_LINE_PROMPT.length(), WIDTH); + checkCursor(buffer, 1, cmd2.length() + MULTI_LINE_PROMPT.length(), WIDTH); + } + + {// Check that the cursor location is on col=MULTI_LINE_PROMPT, row=1 + Buffer buffer = new Buffer(new Prompt(PROMPT)); + String cmd = "cmd --opt1"; + buffer.insert((c) -> { + }, cmd + "\\", WIDTH); + buffer.setMultiLine(true); + buffer.updateMultiLineBuffer(); + int offset = cmd.length() - 1; + check(buffer, offset, 0, cmd.length() + PROMPT.length() - 1, WIDTH); + checkCursor(buffer, 1, MULTI_LINE_PROMPT.length(), WIDTH); + } + + { //Wrapped and multiline. + Buffer buffer = new Buffer(new Prompt(PROMPT)); + String cmd = "cmd --opt1 --opt2 --opt3\\"; + int width = PROMPT.length() + (cmd.length() / 2); + buffer.insert((c) -> { + }, cmd, width); + buffer.setMultiLine(true); + buffer.updateMultiLineBuffer(); + String cmd2 = "--opt4"; + buffer.insert((c) -> { + }, cmd2, width); + int offset = cmd.length() - 1 + cmd2.length(); + check(buffer, offset, 2, cmd2.length() + MULTI_LINE_PROMPT.length(), width); + } + } + + @Test + public void lineTest() throws IOException { + TerminalConnection connection = new TerminalConnection(); + Buffer buffer = new Buffer(new Prompt(PROMPT)); + String cmd1 = "cmd --opt1 --opt2 "; + String cmd2 = "--opt3 --opt4"; + String cmd = cmd1 + cmd2; + buffer.insert((c) -> { + }, cmd1 + "\\", WIDTH); + buffer.setMultiLine(true); + buffer.updateMultiLineBuffer(); + buffer.insert((c) -> { + }, cmd2, WIDTH); + Line line = new Line(buffer, connection, WIDTH); + String s = line.getLineToCursor(); + Assert.assertEquals(cmd, s); + Assert.assertFalse(line.getCursorLocator() == null); + Assert.assertEquals(buffer.multiCursor(), s.length()); + line.newCursorTransactionBuilder().move(10).build().run(); + Assert.assertEquals(buffer.multiCursor(), s.length()); + line.newCursorTransactionBuilder().moveBackward(10).build().run(); + Assert.assertEquals(buffer.multiCursor(), s.length()); + line.newCursorTransactionBuilder().moveForward(10).build().run(); + Assert.assertEquals(buffer.multiCursor(), s.length()); + line.newCursorTransactionBuilder().moveDown(10).build().run(); + Assert.assertEquals(buffer.multiCursor(), s.length()); + line.newCursorTransactionBuilder().moveUp(10).build().run(); + Assert.assertEquals(buffer.multiCursor(), s.length()); + line.newCursorTransactionBuilder().colorize(10, Color.DEFAULT, Color.DEFAULT, + true).build().run(); + Assert.assertEquals(buffer.multiCursor(), s.length()); + } + + private static void checkCursor(Buffer buffer, int row, int col, int width) { + int c = buffer.multiCursor(); + check(buffer, c, row, col, width); + } + + private static void check(Buffer buffer, int offset, int row, int col, int width) { + CursorLocation cursorLoc = buffer.getCursorLocator().locate(offset, width); + assertTrue("Invalid column " + cursorLoc.getColumn() + + ". Expected " + col, + cursorLoc.getColumn() == col); + assertTrue("Invalid row " + cursorLoc.getRow() + ". Expected " + row, + cursorLoc.getRow() == row); + } +}