--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/jdk.internal.le/share/classes/jdk/internal/jline/console/ConsoleReader.java Tue Jul 07 13:17:53 2015 +0200
@@ -0,0 +1,4006 @@
+/*
+ * Copyright (c) 2002-2012, the original author or authors.
+ *
+ * This software is distributable under the BSD license. See the terms of the
+ * BSD license in the documentation provided with this software.
+ *
+ * http://www.opensource.org/licenses/bsd-license.php
+ */
+package jdk.internal.jline.console;
+
+//import java.awt.*;
+//import java.awt.datatransfer.Clipboard;
+//import java.awt.datatransfer.DataFlavor;
+//import java.awt.datatransfer.Transferable;
+//import java.awt.datatransfer.UnsupportedFlavorException;
+//import java.awt.event.ActionListener;
+//import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+//import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+//import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+//import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.Stack;
+import java.util.regex.Pattern;
+
+import jdk.internal.jline.Terminal;
+import jdk.internal.jline.TerminalFactory;
+import jdk.internal.jline.UnixTerminal;
+import jdk.internal.jline.console.completer.CandidateListCompletionHandler;
+import jdk.internal.jline.console.completer.Completer;
+import jdk.internal.jline.console.completer.CompletionHandler;
+import jdk.internal.jline.console.history.History;
+import jdk.internal.jline.console.history.MemoryHistory;
+import jdk.internal.jline.internal.Configuration;
+import jdk.internal.jline.internal.InputStreamReader;
+import jdk.internal.jline.internal.Log;
+import jdk.internal.jline.internal.NonBlockingInputStream;
+import jdk.internal.jline.internal.Nullable;
+import jdk.internal.jline.internal.Urls;
+//import org.fusesource.jansi.AnsiOutputStream;
+
+import static jdk.internal.jline.internal.Preconditions.checkNotNull;
+
+/**
+ * A reader for console applications. It supports custom tab-completion,
+ * saveable command history, and command line editing. On some platforms,
+ * platform-specific commands will need to be issued before the reader will
+ * function properly. See {@link jline.Terminal#init} for convenience
+ * methods for issuing platform-specific setup commands.
+ *
+ * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
+ * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
+ * @author <a href="mailto:gnodet@gmail.com">Guillaume Nodet</a>
+ */
+public class ConsoleReader
+{
+ public static final String JLINE_NOBELL = "jline.nobell";
+
+ public static final String JLINE_ESC_TIMEOUT = "jline.esc.timeout";
+
+ public static final String JLINE_INPUTRC = "jline.inputrc";
+
+ public static final String INPUT_RC = ".inputrc";
+
+ public static final String DEFAULT_INPUT_RC = "/etc/inputrc";
+
+ public static final char BACKSPACE = '\b';
+
+ public static final char RESET_LINE = '\r';
+
+ public static final char KEYBOARD_BELL = '\07';
+
+ public static final char NULL_MASK = 0;
+
+ public static final int TAB_WIDTH = 4;
+
+ private static final ResourceBundle
+ resources = ResourceBundle.getBundle(CandidateListCompletionHandler.class.getName());
+
+ private final Terminal terminal;
+
+ private final Writer out;
+
+ private final CursorBuffer buf = new CursorBuffer();
+
+ private String prompt;
+ private int promptLen;
+
+ private boolean expandEvents = true;
+
+ private boolean bellEnabled = !Configuration.getBoolean(JLINE_NOBELL, true);
+
+ private boolean handleUserInterrupt = false;
+
+ private Character mask;
+
+ private Character echoCharacter;
+
+ private StringBuffer searchTerm = null;
+
+ private String previousSearchTerm = "";
+
+ private int searchIndex = -1;
+
+ private int parenBlinkTimeout = 500;
+
+ /*
+ * The reader and the nonBlockingInput go hand-in-hand. The reader wraps
+ * the nonBlockingInput, but we have to retain a handle to it so that
+ * we can shut down its blocking read thread when we go away.
+ */
+ private NonBlockingInputStream in;
+ private long escapeTimeout;
+ private Reader reader;
+
+ /*
+ * TODO: Please read the comments about this in setInput(), but this needs
+ * to be done away with.
+ */
+ private boolean isUnitTestInput;
+
+ /**
+ * Last character searched for with a vi character search
+ */
+ private char charSearchChar = 0; // Character to search for
+ private char charSearchLastInvokeChar = 0; // Most recent invocation key
+ private char charSearchFirstInvokeChar = 0;// First character that invoked
+
+ /**
+ * The vi yank buffer
+ */
+ private String yankBuffer = "";
+
+ private KillRing killRing = new KillRing();
+
+ private String encoding;
+
+ private boolean recording;
+
+ private String macro = "";
+
+ private String appName;
+
+ private URL inputrcUrl;
+
+ private ConsoleKeys consoleKeys;
+
+ private String commentBegin = null;
+
+ private boolean skipLF = false;
+
+ /**
+ * Set to true if the reader should attempt to detect copy-n-paste. The
+ * effect of this that an attempt is made to detect if tab is quickly
+ * followed by another character, then it is assumed that the tab was
+ * a literal tab as part of a copy-and-paste operation and is inserted as
+ * such.
+ */
+ private boolean copyPasteDetection = false;
+
+ /*
+ * Current internal state of the line reader
+ */
+ private State state = State.NORMAL;
+
+ /**
+ * Possible states in which the current readline operation may be in.
+ */
+ private static enum State {
+ /**
+ * The user is just typing away
+ */
+ NORMAL,
+ /**
+ * In the middle of a emacs seach
+ */
+ SEARCH,
+ FORWARD_SEARCH,
+ /**
+ * VI "yank-to" operation ("y" during move mode)
+ */
+ VI_YANK_TO,
+ /**
+ * VI "delete-to" operation ("d" during move mode)
+ */
+ VI_DELETE_TO,
+ /**
+ * VI "change-to" operation ("c" during move mode)
+ */
+ VI_CHANGE_TO
+ }
+
+ public ConsoleReader() throws IOException {
+ this(null, new FileInputStream(FileDescriptor.in), System.out, null);
+ }
+
+ public ConsoleReader(final InputStream in, final OutputStream out) throws IOException {
+ this(null, in, out, null);
+ }
+
+ public ConsoleReader(final InputStream in, final OutputStream out, final Terminal term) throws IOException {
+ this(null, in, out, term);
+ }
+
+ public ConsoleReader(final @Nullable String appName, final InputStream in, final OutputStream out, final @Nullable Terminal term) throws IOException {
+ this(appName, in, out, term, null);
+ }
+
+ public ConsoleReader(final @Nullable String appName, final InputStream in, final OutputStream out, final @Nullable Terminal term, final @Nullable String encoding)
+ throws IOException
+ {
+ this.appName = appName != null ? appName : "JLine";
+ this.encoding = encoding != null ? encoding : Configuration.getEncoding();
+ this.terminal = term != null ? term : TerminalFactory.get();
+ String outEncoding = terminal.getOutputEncoding() != null? terminal.getOutputEncoding() : this.encoding;
+ this.out = new OutputStreamWriter(terminal.wrapOutIfNeeded(out), outEncoding);
+ setInput( in );
+
+ this.inputrcUrl = getInputRc();
+
+ consoleKeys = new ConsoleKeys(this.appName, inputrcUrl);
+ }
+
+ private URL getInputRc() throws IOException {
+ String path = Configuration.getString(JLINE_INPUTRC);
+ if (path == null) {
+ File f = new File(Configuration.getUserHome(), INPUT_RC);
+ if (!f.exists()) {
+ f = new File(DEFAULT_INPUT_RC);
+ }
+ return f.toURI().toURL();
+ } else {
+ return Urls.create(path);
+ }
+ }
+
+ public KeyMap getKeys() {
+ return consoleKeys.getKeys();
+ }
+
+ void setInput(final InputStream in) throws IOException {
+ this.escapeTimeout = Configuration.getLong(JLINE_ESC_TIMEOUT, 100);
+ /*
+ * This is gross and here is how to fix it. In getCurrentPosition()
+ * and getCurrentAnsiRow(), the logic is disabled when running unit
+ * tests and the fact that it is a unit test is determined by knowing
+ * if the original input stream was a ByteArrayInputStream. So, this
+ * is our test to do this. What SHOULD happen is that the unit
+ * tests should pass in a terminal that is appropriately configured
+ * such that whatever behavior they expect to happen (or not happen)
+ * happens (or doesn't).
+ *
+ * So, TODO, get rid of this and fix the unit tests.
+ */
+ this.isUnitTestInput = in instanceof ByteArrayInputStream;
+ boolean nonBlockingEnabled =
+ escapeTimeout > 0L
+ && terminal.isSupported()
+ && in != null;
+
+ /*
+ * If we had a non-blocking thread already going, then shut it down
+ * and start a new one.
+ */
+ if (this.in != null) {
+ this.in.shutdown();
+ }
+
+ final InputStream wrapped = terminal.wrapInIfNeeded( in );
+
+ this.in = new NonBlockingInputStream(wrapped, nonBlockingEnabled);
+ this.reader = new InputStreamReader( this.in, encoding );
+ }
+
+ /**
+ * Shuts the console reader down. This method should be called when you
+ * have completed using the reader as it shuts down and cleans up resources
+ * that would otherwise be "leaked".
+ */
+ public void shutdown() {
+ if (in != null) {
+ in.shutdown();
+ }
+ }
+
+ /**
+ * Shuts down the ConsoleReader if the JVM attempts to clean it up.
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ shutdown();
+ }
+ finally {
+ super.finalize();
+ }
+ }
+
+ public InputStream getInput() {
+ return in;
+ }
+
+ public Writer getOutput() {
+ return out;
+ }
+
+ public Terminal getTerminal() {
+ return terminal;
+ }
+
+ public CursorBuffer getCursorBuffer() {
+ return buf;
+ }
+
+ public void setExpandEvents(final boolean expand) {
+ this.expandEvents = expand;
+ }
+
+ public boolean getExpandEvents() {
+ return expandEvents;
+ }
+
+ /**
+ * Enables or disables copy and paste detection. The effect of enabling this
+ * this setting is that when a tab is received immediately followed by another
+ * character, the tab will not be treated as a completion, but as a tab literal.
+ * @param onoff true if detection is enabled
+ */
+ public void setCopyPasteDetection(final boolean onoff) {
+ copyPasteDetection = onoff;
+ }
+
+ /**
+ * @return true if copy and paste detection is enabled.
+ */
+ public boolean isCopyPasteDetectionEnabled() {
+ return copyPasteDetection;
+ }
+
+ /**
+ * Set whether the console bell is enabled.
+ *
+ * @param enabled true if enabled; false otherwise
+ * @since 2.7
+ */
+ public void setBellEnabled(boolean enabled) {
+ this.bellEnabled = enabled;
+ }
+
+ /**
+ * Get whether the console bell is enabled
+ *
+ * @return true if enabled; false otherwise
+ * @since 2.7
+ */
+ public boolean getBellEnabled() {
+ return bellEnabled;
+ }
+
+ /**
+ * Set whether user interrupts (ctrl-C) are handled by having JLine
+ * throw {@link UserInterruptException} from {@link #readLine}.
+ * Otherwise, the JVM will handle {@code SIGINT} as normal, which
+ * usually causes it to exit. The default is {@code false}.
+ *
+ * @since 2.10
+ */
+ public void setHandleUserInterrupt(boolean enabled)
+ {
+ this.handleUserInterrupt = enabled;
+ }
+
+ /**
+ * Get whether user interrupt handling is enabled
+ *
+ * @return true if enabled; false otherwise
+ * @since 2.10
+ */
+ public boolean getHandleUserInterrupt()
+ {
+ return handleUserInterrupt;
+ }
+
+ /**
+ * Sets the string that will be used to start a comment when the
+ * insert-comment key is struck.
+ * @param commentBegin The begin comment string.
+ * @since 2.7
+ */
+ public void setCommentBegin(String commentBegin) {
+ this.commentBegin = commentBegin;
+ }
+
+ /**
+ * @return the string that will be used to start a comment when the
+ * insert-comment key is struck.
+ * @since 2.7
+ */
+ public String getCommentBegin() {
+ String str = commentBegin;
+
+ if (str == null) {
+ str = consoleKeys.getVariable("comment-begin");
+ if (str == null) {
+ str = "#";
+ }
+ }
+ return str;
+ }
+
+ public void setPrompt(final String prompt) {
+ this.prompt = prompt;
+ this.promptLen = ((prompt == null) ? 0 : stripAnsi(lastLine(prompt)).length());
+ }
+
+ public String getPrompt() {
+ return prompt;
+ }
+
+ /**
+ * Set the echo character. For example, to have "*" entered when a password is typed:
+ * <p/>
+ * <pre>
+ * myConsoleReader.setEchoCharacter(new Character('*'));
+ * </pre>
+ * <p/>
+ * Setting the character to
+ * <p/>
+ * <pre>
+ * null
+ * </pre>
+ * <p/>
+ * will restore normal character echoing. Setting the character to
+ * <p/>
+ * <pre>
+ * new Character(0)
+ * </pre>
+ * <p/>
+ * will cause nothing to be echoed.
+ *
+ * @param c the character to echo to the console in place of the typed character.
+ */
+ public void setEchoCharacter(final Character c) {
+ this.echoCharacter = c;
+ }
+
+ /**
+ * Returns the echo character.
+ */
+ public Character getEchoCharacter() {
+ return echoCharacter;
+ }
+
+ /**
+ * Erase the current line.
+ *
+ * @return false if we failed (e.g., the buffer was empty)
+ */
+ protected final boolean resetLine() throws IOException {
+ if (buf.cursor == 0) {
+ return false;
+ }
+
+ StringBuilder killed = new StringBuilder();
+
+ while (buf.cursor > 0) {
+ char c = buf.current();
+ if (c == 0) {
+ break;
+ }
+
+ killed.append(c);
+ backspace();
+ }
+
+ String copy = killed.reverse().toString();
+ killRing.addBackwards(copy);
+
+ return true;
+ }
+
+ int getCursorPosition() {
+ // FIXME: does not handle anything but a line with a prompt absolute position
+ return promptLen + buf.cursor;
+ }
+
+ /**
+ * Returns the text after the last '\n'.
+ * prompt is returned if no '\n' characters are present.
+ * null is returned if prompt is null.
+ */
+ private String lastLine(String str) {
+ if (str == null) return "";
+ int last = str.lastIndexOf("\n");
+
+ if (last >= 0) {
+ return str.substring(last + 1, str.length());
+ }
+
+ return str;
+ }
+
+ String stripAnsi(String str) {
+ if (str == null) return "";
+ return ANSI_CODE_PATTERN.matcher(str).replaceAll("");
+// try {
+// ByteArrayOutputStream baos = new ByteArrayOutputStream();
+// AnsiOutputStream aos = new AnsiOutputStream(baos);
+// aos.write(str.getBytes());
+// aos.flush();
+// return baos.toString();
+// } catch (IOException e) {
+// return str;
+// }
+ }
+ //where:
+ private static final Pattern ANSI_CODE_PATTERN = Pattern.compile("\033\\[[^@-~]*[@-~]");
+
+ /**
+ * Move the cursor position to the specified absolute index.
+ */
+ public final boolean setCursorPosition(final int position) throws IOException {
+ if (position == buf.cursor) {
+ return true;
+ }
+
+ return moveCursor(position - buf.cursor) != 0;
+ }
+
+ /**
+ * Set the current buffer's content to the specified {@link String}. The
+ * visual console will be modified to show the current buffer.
+ *
+ * @param buffer the new contents of the buffer.
+ */
+ private void setBuffer(final String buffer) throws IOException {
+ // don't bother modifying it if it is unchanged
+ if (buffer.equals(buf.buffer.toString())) {
+ return;
+ }
+
+ // obtain the difference between the current buffer and the new one
+ int sameIndex = 0;
+
+ for (int i = 0, l1 = buffer.length(), l2 = buf.buffer.length(); (i < l1)
+ && (i < l2); i++) {
+ if (buffer.charAt(i) == buf.buffer.charAt(i)) {
+ sameIndex++;
+ }
+ else {
+ break;
+ }
+ }
+
+ int diff = buf.cursor - sameIndex;
+ if (diff < 0) { // we can't backspace here so try from the end of the buffer
+ moveToEnd();
+ diff = buf.buffer.length() - sameIndex;
+ }
+
+ backspace(diff); // go back for the differences
+ killLine(); // clear to the end of the line
+ buf.buffer.setLength(sameIndex); // the new length
+ putString(buffer.substring(sameIndex)); // append the differences
+ }
+
+ private void setBuffer(final CharSequence buffer) throws IOException {
+ setBuffer(String.valueOf(buffer));
+ }
+
+ private void setBufferKeepPos(final String buffer) throws IOException {
+ int pos = buf.cursor;
+ setBuffer(buffer);
+ setCursorPosition(pos);
+ }
+
+ private void setBufferKeepPos(final CharSequence buffer) throws IOException {
+ setBufferKeepPos(String.valueOf(buffer));
+ }
+
+ /**
+ * Output put the prompt + the current buffer
+ */
+ public final void drawLine() throws IOException {
+ String prompt = getPrompt();
+ if (prompt != null) {
+ print(prompt);
+ }
+
+ print(buf.buffer.toString());
+
+ if (buf.length() != buf.cursor) { // not at end of line
+ back(buf.length() - buf.cursor - 1);
+ }
+ // force drawBuffer to check for weird wrap (after clear screen)
+ drawBuffer();
+ }
+
+ /**
+ * Clear the line and redraw it.
+ */
+ public final void redrawLine() throws IOException {
+ print(RESET_LINE);
+// flush();
+ drawLine();
+ }
+
+ /**
+ * Clear the buffer and add its contents to the history.
+ *
+ * @return the former contents of the buffer.
+ */
+ final String finishBuffer() throws IOException { // FIXME: Package protected because used by tests
+ String str = buf.buffer.toString();
+ String historyLine = str;
+
+ if (expandEvents) {
+ try {
+ str = expandEvents(str);
+ // all post-expansion occurrences of '!' must have been escaped, so re-add escape to each
+ historyLine = str.replace("!", "\\!");
+ // only leading '^' results in expansion, so only re-add escape for that case
+ historyLine = historyLine.replaceAll("^\\^", "\\\\^");
+ } catch(IllegalArgumentException e) {
+ Log.error("Could not expand event", e);
+ beep();
+ buf.clear();
+ str = "";
+ }
+ }
+
+ // we only add it to the history if the buffer is not empty
+ // and if mask is null, since having a mask typically means
+ // the string was a password. We clear the mask after this call
+ if (str.length() > 0) {
+ if (mask == null && isHistoryEnabled()) {
+ history.add(historyLine);
+ }
+ else {
+ mask = null;
+ }
+ }
+
+ history.moveToEnd();
+
+ buf.buffer.setLength(0);
+ buf.cursor = 0;
+
+ return str;
+ }
+
+ /**
+ * Expand event designator such as !!, !#, !3, etc...
+ * See http://www.gnu.org/software/bash/manual/html_node/Event-Designators.html
+ */
+ @SuppressWarnings("fallthrough")
+ protected String expandEvents(String str) throws IOException {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < str.length(); i++) {
+ char c = str.charAt(i);
+ switch (c) {
+ case '\\':
+ // any '\!' should be considered an expansion escape, so skip expansion and strip the escape character
+ // a leading '\^' should be considered an expansion escape, so skip expansion and strip the escape character
+ // otherwise, add the escape
+ if (i + 1 < str.length()) {
+ char nextChar = str.charAt(i+1);
+ if (nextChar == '!' || (nextChar == '^' && i == 0)) {
+ c = nextChar;
+ i++;
+ }
+ }
+ sb.append(c);
+ break;
+ case '!':
+ if (i + 1 < str.length()) {
+ c = str.charAt(++i);
+ boolean neg = false;
+ String rep = null;
+ int i1, idx;
+ switch (c) {
+ case '!':
+ if (history.size() == 0) {
+ throw new IllegalArgumentException("!!: event not found");
+ }
+ rep = history.get(history.index() - 1).toString();
+ break;
+ case '#':
+ sb.append(sb.toString());
+ break;
+ case '?':
+ i1 = str.indexOf('?', i + 1);
+ if (i1 < 0) {
+ i1 = str.length();
+ }
+ String sc = str.substring(i + 1, i1);
+ i = i1;
+ idx = searchBackwards(sc);
+ if (idx < 0) {
+ throw new IllegalArgumentException("!?" + sc + ": event not found");
+ } else {
+ rep = history.get(idx).toString();
+ }
+ break;
+ case '$':
+ if (history.size() == 0) {
+ throw new IllegalArgumentException("!$: event not found");
+ }
+ String previous = history.get(history.index() - 1).toString().trim();
+ int lastSpace = previous.lastIndexOf(' ');
+ if(lastSpace != -1) {
+ rep = previous.substring(lastSpace+1);
+ } else {
+ rep = previous;
+ }
+ break;
+ case ' ':
+ case '\t':
+ sb.append('!');
+ sb.append(c);
+ break;
+ case '-':
+ neg = true;
+ i++;
+ // fall through
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ i1 = i;
+ for (; i < str.length(); i++) {
+ c = str.charAt(i);
+ if (c < '0' || c > '9') {
+ break;
+ }
+ }
+ idx = 0;
+ try {
+ idx = Integer.parseInt(str.substring(i1, i));
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found");
+ }
+ if (neg) {
+ if (idx > 0 && idx <= history.size()) {
+ rep = (history.get(history.index() - idx)).toString();
+ } else {
+ throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found");
+ }
+ } else {
+ if (idx > history.index() - history.size() && idx <= history.index()) {
+ rep = (history.get(idx - 1)).toString();
+ } else {
+ throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found");
+ }
+ }
+ break;
+ default:
+ String ss = str.substring(i);
+ i = str.length();
+ idx = searchBackwards(ss, history.index(), true);
+ if (idx < 0) {
+ throw new IllegalArgumentException("!" + ss + ": event not found");
+ } else {
+ rep = history.get(idx).toString();
+ }
+ break;
+ }
+ if (rep != null) {
+ sb.append(rep);
+ }
+ } else {
+ sb.append(c);
+ }
+ break;
+ case '^':
+ if (i == 0) {
+ int i1 = str.indexOf('^', i + 1);
+ int i2 = str.indexOf('^', i1 + 1);
+ if (i2 < 0) {
+ i2 = str.length();
+ }
+ if (i1 > 0 && i2 > 0) {
+ String s1 = str.substring(i + 1, i1);
+ String s2 = str.substring(i1 + 1, i2);
+ String s = history.get(history.index() - 1).toString().replace(s1, s2);
+ sb.append(s);
+ i = i2 + 1;
+ break;
+ }
+ }
+ sb.append(c);
+ break;
+ default:
+ sb.append(c);
+ break;
+ }
+ }
+ String result = sb.toString();
+ if (!str.equals(result)) {
+ print(result);
+ println();
+ flush();
+ }
+ return result;
+
+ }
+
+ /**
+ * Write out the specified string to the buffer and the output stream.
+ */
+ public final void putString(final CharSequence str) throws IOException {
+ buf.write(str);
+ if (mask == null) {
+ // no masking
+ print(str);
+ } else if (mask == NULL_MASK) {
+ // don't print anything
+ } else {
+ print(mask, str.length());
+ }
+ drawBuffer();
+ }
+
+ /**
+ * Redraw the rest of the buffer from the cursor onwards. This is necessary
+ * for inserting text into the buffer.
+ *
+ * @param clear the number of characters to clear after the end of the buffer
+ */
+ private void drawBuffer(final int clear) throws IOException {
+ // debug ("drawBuffer: " + clear);
+ if (buf.cursor == buf.length() && clear == 0) {
+ } else {
+ char[] chars = buf.buffer.substring(buf.cursor).toCharArray();
+ if (mask != null) {
+ Arrays.fill(chars, mask);
+ }
+ if (terminal.hasWeirdWrap()) {
+ // need to determine if wrapping will occur:
+ int width = terminal.getWidth();
+ int pos = getCursorPosition();
+ for (int i = 0; i < chars.length; i++) {
+ print(chars[i]);
+ if ((pos + i + 1) % width == 0) {
+ print(32); // move cursor to next line by printing dummy space
+ print(13); // CR / not newline.
+ }
+ }
+ } else {
+ print(chars);
+ }
+ clearAhead(clear, chars.length);
+ if (terminal.isAnsiSupported()) {
+ if (chars.length > 0) {
+ back(chars.length);
+ }
+ } else {
+ back(chars.length);
+ }
+ }
+ if (terminal.hasWeirdWrap()) {
+ int width = terminal.getWidth();
+ // best guess on whether the cursor is in that weird location...
+ // Need to do this without calling ansi cursor location methods
+ // otherwise it breaks paste of wrapped lines in xterm.
+ if (getCursorPosition() > 0 && (getCursorPosition() % width == 0)
+ && buf.cursor == buf.length() && clear == 0) {
+ // the following workaround is reverse-engineered from looking
+ // at what bash sent to the terminal in the same situation
+ print(32); // move cursor to next line by printing dummy space
+ print(13); // CR / not newline.
+ }
+ }
+ }
+
+ /**
+ * Redraw the rest of the buffer from the cursor onwards. This is necessary
+ * for inserting text into the buffer.
+ */
+ private void drawBuffer() throws IOException {
+ drawBuffer(0);
+ }
+
+ /**
+ * Clear ahead the specified number of characters without moving the cursor.
+ *
+ * @param num the number of characters to clear
+ * @param delta the difference between the internal cursor and the screen
+ * cursor - if > 0, assume some stuff was printed and weird wrap has to be
+ * checked
+ */
+ private void clearAhead(final int num, int delta) throws IOException {
+ if (num == 0) {
+ return;
+ }
+
+ if (terminal.isAnsiSupported()) {
+ int width = terminal.getWidth();
+ int screenCursorCol = getCursorPosition() + delta;
+ // clear current line
+ printAnsiSequence("K");
+ // if cursor+num wraps, then we need to clear the line(s) below too
+ int curCol = screenCursorCol % width;
+ int endCol = (screenCursorCol + num - 1) % width;
+ int lines = num / width;
+ if (endCol < curCol) lines++;
+ for (int i = 0; i < lines; i++) {
+ printAnsiSequence("B");
+ printAnsiSequence("2K");
+ }
+ for (int i = 0; i < lines; i++) {
+ printAnsiSequence("A");
+ }
+ return;
+ }
+
+ // print blank extra characters
+ print(' ', num);
+
+ // we need to flush here so a "clever" console doesn't just ignore the redundancy
+ // of a space followed by a backspace.
+// flush();
+
+ // reset the visual cursor
+ back(num);
+
+// flush();
+ }
+
+ /**
+ * Move the visual cursor backwards without modifying the buffer cursor.
+ */
+ protected void back(final int num) throws IOException {
+ if (num == 0) return;
+ if (terminal.isAnsiSupported()) {
+ int width = getTerminal().getWidth();
+ int cursor = getCursorPosition();
+ int realCursor = cursor + num;
+ int realCol = realCursor % width;
+ int newCol = cursor % width;
+ int moveup = num / width;
+ int delta = realCol - newCol;
+ if (delta < 0) moveup++;
+ if (moveup > 0) {
+ printAnsiSequence(moveup + "A");
+ }
+ printAnsiSequence((1 + newCol) + "G");
+ return;
+ }
+ print(BACKSPACE, num);
+// flush();
+ }
+
+ /**
+ * Flush the console output stream. This is important for printout out single characters (like a backspace or
+ * keyboard) that we want the console to handle immediately.
+ */
+ public void flush() throws IOException {
+ out.flush();
+ }
+
+ private int backspaceAll() throws IOException {
+ return backspace(Integer.MAX_VALUE);
+ }
+
+ /**
+ * Issue <em>num</em> backspaces.
+ *
+ * @return the number of characters backed up
+ */
+ private int backspace(final int num) throws IOException {
+ if (buf.cursor == 0) {
+ return 0;
+ }
+
+ int count = 0;
+
+ int termwidth = getTerminal().getWidth();
+ int lines = getCursorPosition() / termwidth;
+ count = moveCursor(-1 * num) * -1;
+ buf.buffer.delete(buf.cursor, buf.cursor + count);
+ if (getCursorPosition() / termwidth != lines) {
+ if (terminal.isAnsiSupported()) {
+ // debug("doing backspace redraw: " + getCursorPosition() + " on " + termwidth + ": " + lines);
+ printAnsiSequence("K");
+ // if cursor+num wraps, then we need to clear the line(s) below too
+ // last char printed is one pos less than cursor so we subtract
+ // one
+/*
+ // TODO: fixme (does not work - test with reverse search with wrapping line and CTRL-E)
+ int endCol = (getCursorPosition() + num - 1) % termwidth;
+ int curCol = getCursorPosition() % termwidth;
+ if (endCol < curCol) lines++;
+ for (int i = 1; i < lines; i++) {
+ printAnsiSequence("B");
+ printAnsiSequence("2K");
+ }
+ for (int i = 1; i < lines; i++) {
+ printAnsiSequence("A");
+ }
+ return count;
+*/
+ }
+ }
+ drawBuffer(count);
+
+ return count;
+ }
+
+ /**
+ * Issue a backspace.
+ *
+ * @return true if successful
+ */
+ public boolean backspace() throws IOException {
+ return backspace(1) == 1;
+ }
+
+ protected boolean moveToEnd() throws IOException {
+ if (buf.cursor == buf.length()) {
+ return true;
+ }
+ return moveCursor(buf.length() - buf.cursor) > 0;
+ }
+
+ /**
+ * Delete the character at the current position and redraw the remainder of the buffer.
+ */
+ private boolean deleteCurrentCharacter() throws IOException {
+ if (buf.length() == 0 || buf.cursor == buf.length()) {
+ return false;
+ }
+
+ buf.buffer.deleteCharAt(buf.cursor);
+ drawBuffer(1);
+ return true;
+ }
+
+ /**
+ * This method is calling while doing a delete-to ("d"), change-to ("c"),
+ * or yank-to ("y") and it filters out only those movement operations
+ * that are allowable during those operations. Any operation that isn't
+ * allow drops you back into movement mode.
+ *
+ * @param op The incoming operation to remap
+ * @return The remaped operation
+ */
+ private Operation viDeleteChangeYankToRemap (Operation op) {
+ switch (op) {
+ case VI_EOF_MAYBE:
+ case ABORT:
+ case BACKWARD_CHAR:
+ case FORWARD_CHAR:
+ case END_OF_LINE:
+ case VI_MATCH:
+ case VI_BEGNNING_OF_LINE_OR_ARG_DIGIT:
+ case VI_ARG_DIGIT:
+ case VI_PREV_WORD:
+ case VI_END_WORD:
+ case VI_CHAR_SEARCH:
+ case VI_NEXT_WORD:
+ case VI_FIRST_PRINT:
+ case VI_GOTO_MARK:
+ case VI_COLUMN:
+ case VI_DELETE_TO:
+ case VI_YANK_TO:
+ case VI_CHANGE_TO:
+ return op;
+
+ default:
+ return Operation.VI_MOVEMENT_MODE;
+ }
+ }
+
+ /**
+ * Deletes the previous character from the cursor position
+ * @param count number of times to do it.
+ * @return true if it was done.
+ * @throws IOException
+ */
+ private boolean viRubout(int count) throws IOException {
+ boolean ok = true;
+ for (int i = 0; ok && i < count; i++) {
+ ok = backspace();
+ }
+ return ok;
+ }
+
+ /**
+ * Deletes the character you are sitting on and sucks the rest of
+ * the line in from the right.
+ * @param count Number of times to perform the operation.
+ * @return true if its works, false if it didn't
+ * @throws IOException
+ */
+ private boolean viDelete(int count) throws IOException {
+ boolean ok = true;
+ for (int i = 0; ok && i < count; i++) {
+ ok = deleteCurrentCharacter();
+ }
+ return ok;
+ }
+
+ /**
+ * Switches the case of the current character from upper to lower
+ * or lower to upper as necessary and advances the cursor one
+ * position to the right.
+ * @param count The number of times to repeat
+ * @return true if it completed successfully, false if not all
+ * case changes could be completed.
+ * @throws IOException
+ */
+ private boolean viChangeCase(int count) throws IOException {
+ boolean ok = true;
+ for (int i = 0; ok && i < count; i++) {
+
+ ok = buf.cursor < buf.buffer.length ();
+ if (ok) {
+ char ch = buf.buffer.charAt(buf.cursor);
+ if (Character.isUpperCase(ch)) {
+ ch = Character.toLowerCase(ch);
+ }
+ else if (Character.isLowerCase(ch)) {
+ ch = Character.toUpperCase(ch);
+ }
+ buf.buffer.setCharAt(buf.cursor, ch);
+ drawBuffer(1);
+ moveCursor(1);
+ }
+ }
+ return ok;
+ }
+
+ /**
+ * Implements the vi change character command (in move-mode "r"
+ * followed by the character to change to).
+ * @param count Number of times to perform the action
+ * @param c The character to change to
+ * @return Whether or not there were problems encountered
+ * @throws IOException
+ */
+ private boolean viChangeChar(int count, int c) throws IOException {
+ // EOF, ESC, or CTRL-C aborts.
+ if (c < 0 || c == '\033' || c == '\003') {
+ return true;
+ }
+
+ boolean ok = true;
+ for (int i = 0; ok && i < count; i++) {
+ ok = buf.cursor < buf.buffer.length ();
+ if (ok) {
+ buf.buffer.setCharAt(buf.cursor, (char) c);
+ drawBuffer(1);
+ if (i < (count-1)) {
+ moveCursor(1);
+ }
+ }
+ }
+ return ok;
+ }
+
+ /**
+ * This is a close facsimile of the actual vi previous word logic. In
+ * actual vi words are determined by boundaries of identity characterse.
+ * This logic is a bit more simple and simply looks at white space or
+ * digits or characters. It should be revised at some point.
+ *
+ * @param count number of iterations
+ * @return true if the move was successful, false otherwise
+ * @throws IOException
+ */
+ private boolean viPreviousWord(int count) throws IOException {
+ boolean ok = true;
+ if (buf.cursor == 0) {
+ return false;
+ }
+
+ int pos = buf.cursor - 1;
+ for (int i = 0; pos > 0 && i < count; i++) {
+ // If we are on white space, then move back.
+ while (pos > 0 && isWhitespace(buf.buffer.charAt(pos))) {
+ --pos;
+ }
+
+ while (pos > 0 && !isDelimiter(buf.buffer.charAt(pos-1))) {
+ --pos;
+ }
+
+ if (pos > 0 && i < (count-1)) {
+ --pos;
+ }
+ }
+ setCursorPosition(pos);
+ return ok;
+ }
+
+ /**
+ * Performs the vi "delete-to" action, deleting characters between a given
+ * span of the input line.
+ * @param startPos The start position
+ * @param endPos The end position.
+ * @param isChange If true, then the delete is part of a change operationg
+ * (e.g. "c$" is change-to-end-of line, so we first must delete to end
+ * of line to start the change
+ * @return true if it succeeded, false otherwise
+ * @throws IOException
+ */
+ private boolean viDeleteTo(int startPos, int endPos, boolean isChange) throws IOException {
+ if (startPos == endPos) {
+ return true;
+ }
+
+ if (endPos < startPos) {
+ int tmp = endPos;
+ endPos = startPos;
+ startPos = tmp;
+ }
+
+ setCursorPosition(startPos);
+ buf.cursor = startPos;
+ buf.buffer.delete(startPos, endPos);
+ drawBuffer(endPos - startPos);
+
+ // If we are doing a delete operation (e.g. "d$") then don't leave the
+ // cursor dangling off the end. In reality the "isChange" flag is silly
+ // what is really happening is that if we are in "move-mode" then the
+ // cursor can't be moved off the end of the line, but in "edit-mode" it
+ // is ok, but I have no easy way of knowing which mode we are in.
+ if (! isChange && startPos > 0 && startPos == buf.length()) {
+ moveCursor(-1);
+ }
+ return true;
+ }
+
+ /**
+ * Implement the "vi" yank-to operation. This operation allows you
+ * to yank the contents of the current line based upon a move operation,
+ * for exaple "yw" yanks the current word, "3yw" yanks 3 words, etc.
+ *
+ * @param startPos The starting position from which to yank
+ * @param endPos The ending position to which to yank
+ * @return true if the yank succeeded
+ * @throws IOException
+ */
+ private boolean viYankTo(int startPos, int endPos) throws IOException {
+ int cursorPos = startPos;
+
+ if (endPos < startPos) {
+ int tmp = endPos;
+ endPos = startPos;
+ startPos = tmp;
+ }
+
+ if (startPos == endPos) {
+ yankBuffer = "";
+ return true;
+ }
+
+ yankBuffer = buf.buffer.substring(startPos, endPos);
+
+ /*
+ * It was a movement command that moved the cursor to find the
+ * end position, so put the cursor back where it started.
+ */
+ setCursorPosition(cursorPos);
+ return true;
+ }
+
+ /**
+ * Pasts the yank buffer to the right of the current cursor position
+ * and moves the cursor to the end of the pasted region.
+ *
+ * @param count Number of times to perform the operation.
+ * @return true if it worked, false otherwise
+ * @throws IOException
+ */
+ private boolean viPut(int count) throws IOException {
+ if (yankBuffer.length () == 0) {
+ return true;
+ }
+ if (buf.cursor < buf.buffer.length ()) {
+ moveCursor(1);
+ }
+ for (int i = 0; i < count; i++) {
+ putString(yankBuffer);
+ }
+ moveCursor(-1);
+ return true;
+ }
+
+ /**
+ * Searches forward of the current position for a character and moves
+ * the cursor onto it.
+ * @param count Number of times to repeat the process.
+ * @param ch The character to search for
+ * @return true if the char was found, false otherwise
+ * @throws IOException
+ */
+ private boolean viCharSearch(int count, int invokeChar, int ch) throws IOException {
+ if (ch < 0 || invokeChar < 0) {
+ return false;
+ }
+
+ char searchChar = (char)ch;
+ boolean isForward;
+ boolean stopBefore;
+
+ /*
+ * The character stuff turns out to be hairy. Here is how it works:
+ * f - search forward for ch
+ * F - search backward for ch
+ * t - search forward for ch, but stop just before the match
+ * T - search backward for ch, but stop just after the match
+ * ; - After [fFtT;], repeat the last search, after ',' reverse it
+ * , - After [fFtT;], reverse the last search, after ',' repeat it
+ */
+ if (invokeChar == ';' || invokeChar == ',') {
+ // No recent search done? Then bail
+ if (charSearchChar == 0) {
+ return false;
+ }
+
+ // Reverse direction if switching between ',' and ';'
+ if (charSearchLastInvokeChar == ';' || charSearchLastInvokeChar == ',') {
+ if (charSearchLastInvokeChar != invokeChar) {
+ charSearchFirstInvokeChar = switchCase(charSearchFirstInvokeChar);
+ }
+ }
+ else {
+ if (invokeChar == ',') {
+ charSearchFirstInvokeChar = switchCase(charSearchFirstInvokeChar);
+ }
+ }
+
+ searchChar = charSearchChar;
+ }
+ else {
+ charSearchChar = searchChar;
+ charSearchFirstInvokeChar = (char) invokeChar;
+ }
+
+ charSearchLastInvokeChar = (char)invokeChar;
+
+ isForward = Character.isLowerCase(charSearchFirstInvokeChar);
+ stopBefore = (Character.toLowerCase(charSearchFirstInvokeChar) == 't');
+
+ boolean ok = false;
+
+ if (isForward) {
+ while (count-- > 0) {
+ int pos = buf.cursor + 1;
+ while (pos < buf.buffer.length()) {
+ if (buf.buffer.charAt(pos) == searchChar) {
+ setCursorPosition(pos);
+ ok = true;
+ break;
+ }
+ ++pos;
+ }
+ }
+
+ if (ok) {
+ if (stopBefore)
+ moveCursor(-1);
+
+ /*
+ * When in yank-to, move-to, del-to state we actually want to
+ * go to the character after the one we landed on to make sure
+ * that the character we ended up on is included in the
+ * operation
+ */
+ if (isInViMoveOperationState()) {
+ moveCursor(1);
+ }
+ }
+ }
+ else {
+ while (count-- > 0) {
+ int pos = buf.cursor - 1;
+ while (pos >= 0) {
+ if (buf.buffer.charAt(pos) == searchChar) {
+ setCursorPosition(pos);
+ ok = true;
+ break;
+ }
+ --pos;
+ }
+ }
+
+ if (ok && stopBefore)
+ moveCursor(1);
+ }
+
+ return ok;
+ }
+
+ private char switchCase(char ch) {
+ if (Character.isUpperCase(ch)) {
+ return Character.toLowerCase(ch);
+ }
+ return Character.toUpperCase(ch);
+ }
+
+ /**
+ * @return true if line reader is in the middle of doing a change-to
+ * delete-to or yank-to.
+ */
+ private final boolean isInViMoveOperationState() {
+ return state == State.VI_CHANGE_TO
+ || state == State.VI_DELETE_TO
+ || state == State.VI_YANK_TO;
+ }
+
+ /**
+ * This is a close facsimile of the actual vi next word logic.
+ * As with viPreviousWord() this probably needs to be improved
+ * at some point.
+ *
+ * @param count number of iterations
+ * @return true if the move was successful, false otherwise
+ * @throws IOException
+ */
+ private boolean viNextWord(int count) throws IOException {
+ int pos = buf.cursor;
+ int end = buf.buffer.length();
+
+ for (int i = 0; pos < end && i < count; i++) {
+ // Skip over letter/digits
+ while (pos < end && !isDelimiter(buf.buffer.charAt(pos))) {
+ ++pos;
+ }
+
+ /*
+ * Don't you love special cases? During delete-to and yank-to
+ * operations the word movement is normal. However, during a
+ * change-to, the trailing spaces behind the last word are
+ * left in tact.
+ */
+ if (i < (count-1) || !(state == State.VI_CHANGE_TO)) {
+ while (pos < end && isDelimiter(buf.buffer.charAt(pos))) {
+ ++pos;
+ }
+ }
+ }
+
+ setCursorPosition(pos);
+ return true;
+ }
+
+ /**
+ * Implements a close facsimile of the vi end-of-word movement.
+ * If the character is on white space, it takes you to the end
+ * of the next word. If it is on the last character of a word
+ * it takes you to the next of the next word. Any other character
+ * of a word, takes you to the end of the current word.
+ *
+ * @param count Number of times to repeat the action
+ * @return true if it worked.
+ * @throws IOException
+ */
+ private boolean viEndWord(int count) throws IOException {
+ int pos = buf.cursor;
+ int end = buf.buffer.length();
+
+ for (int i = 0; pos < end && i < count; i++) {
+ if (pos < (end-1)
+ && !isDelimiter(buf.buffer.charAt(pos))
+ && isDelimiter(buf.buffer.charAt (pos+1))) {
+ ++pos;
+ }
+
+ // If we are on white space, then move back.
+ while (pos < end && isDelimiter(buf.buffer.charAt(pos))) {
+ ++pos;
+ }
+
+ while (pos < (end-1) && !isDelimiter(buf.buffer.charAt(pos+1))) {
+ ++pos;
+ }
+ }
+ setCursorPosition(pos);
+ return true;
+ }
+
+ private boolean previousWord() throws IOException {
+ while (isDelimiter(buf.current()) && (moveCursor(-1) != 0)) {
+ // nothing
+ }
+
+ while (!isDelimiter(buf.current()) && (moveCursor(-1) != 0)) {
+ // nothing
+ }
+
+ return true;
+ }
+
+ private boolean nextWord() throws IOException {
+ while (isDelimiter(buf.nextChar()) && (moveCursor(1) != 0)) {
+ // nothing
+ }
+
+ while (!isDelimiter(buf.nextChar()) && (moveCursor(1) != 0)) {
+ // nothing
+ }
+
+ return true;
+ }
+
+ /**
+ * Deletes to the beginning of the word that the cursor is sitting on.
+ * If the cursor is on white-space, it deletes that and to the beginning
+ * of the word before it. If the user is not on a word or whitespace
+ * it deletes up to the end of the previous word.
+ *
+ * @param count Number of times to perform the operation
+ * @return true if it worked, false if you tried to delete too many words
+ * @throws IOException
+ */
+ private boolean unixWordRubout(int count) throws IOException {
+ boolean success = true;
+ StringBuilder killed = new StringBuilder();
+
+ for (; count > 0; --count) {
+ if (buf.cursor == 0) {
+ success = false;
+ break;
+ }
+
+ while (isWhitespace(buf.current())) {
+ char c = buf.current();
+ if (c == 0) {
+ break;
+ }
+
+ killed.append(c);
+ backspace();
+ }
+
+ while (!isWhitespace(buf.current())) {
+ char c = buf.current();
+ if (c == 0) {
+ break;
+ }
+
+ killed.append(c);
+ backspace();
+ }
+ }
+
+ String copy = killed.reverse().toString();
+ killRing.addBackwards(copy);
+
+ return success;
+ }
+
+ private String insertComment(boolean isViMode) throws IOException {
+ String comment = this.getCommentBegin ();
+ setCursorPosition(0);
+ putString(comment);
+ if (isViMode) {
+ consoleKeys.setKeyMap(KeyMap.VI_INSERT);
+ }
+ return accept();
+ }
+
+ /**
+ * Similar to putString() but allows the string to be repeated a specific
+ * number of times, allowing easy support of vi digit arguments to a given
+ * command. The string is placed as the current cursor position.
+ *
+ * @param count The count of times to insert the string.
+ * @param str The string to insert
+ * @return true if the operation is a success, false otherwise
+ * @throws IOException
+ */
+ private boolean insert(int count, final CharSequence str) throws IOException {
+ for (int i = 0; i < count; i++) {
+ buf.write(str);
+ if (mask == null) {
+ // no masking
+ print(str);
+ } else if (mask == NULL_MASK) {
+ // don't print anything
+ } else {
+ print(mask, str.length());
+ }
+ }
+ drawBuffer();
+ return true;
+ }
+
+ /**
+ * Implements vi search ("/" or "?").
+ * @throws IOException
+ */
+ @SuppressWarnings("fallthrough")
+ private int viSearch(char searchChar) throws IOException {
+ boolean isForward = (searchChar == '/');
+
+ /*
+ * This is a little gross, I'm sure there is a more appropriate way
+ * of saving and restoring state.
+ */
+ CursorBuffer origBuffer = buf.copy();
+
+ // Clear the contents of the current line and
+ setCursorPosition (0);
+ killLine();
+
+ // Our new "prompt" is the character that got us into search mode.
+ putString(Character.toString(searchChar));
+ flush();
+
+ boolean isAborted = false;
+ boolean isComplete = false;
+
+ /*
+ * Readline doesn't seem to do any special character map handling
+ * here, so I think we are safe.
+ */
+ int ch = -1;
+ while (!isAborted && !isComplete && (ch = readCharacter()) != -1) {
+ switch (ch) {
+ case '\033': // ESC
+ /*
+ * The ESC behavior doesn't appear to be readline behavior,
+ * but it is a little tweak of my own. I like it.
+ */
+ isAborted = true;
+ break;
+ case '\010': // Backspace
+ case '\177': // Delete
+ backspace();
+ /*
+ * Backspacing through the "prompt" aborts the search.
+ */
+ if (buf.cursor == 0) {
+ isAborted = true;
+ }
+ break;
+ case '\012': // NL
+ case '\015': // CR
+ isComplete = true;
+ break;
+ default:
+ putString(Character.toString((char) ch));
+ }
+
+ flush();
+ }
+
+ // If we aborted, then put ourself at the end of the original buffer.
+ if (ch == -1 || isAborted) {
+ setCursorPosition(0);
+ killLine();
+ putString(origBuffer.buffer);
+ setCursorPosition(origBuffer.cursor);
+ return -1;
+ }
+
+ /*
+ * The first character of the buffer was the search character itself
+ * so we discard it.
+ */
+ String searchTerm = buf.buffer.substring(1);
+ int idx = -1;
+
+ /*
+ * The semantics of the history thing is gross when you want to
+ * explicitly iterate over entries (without an iterator) as size()
+ * returns the actual number of entries in the list but get()
+ * doesn't work the way you think.
+ */
+ int end = history.index();
+ int start = (end <= history.size()) ? 0 : end - history.size();
+
+ if (isForward) {
+ for (int i = start; i < end; i++) {
+ if (history.get(i).toString().contains(searchTerm)) {
+ idx = i;
+ break;
+ }
+ }
+ }
+ else {
+ for (int i = end-1; i >= start; i--) {
+ if (history.get(i).toString().contains(searchTerm)) {
+ idx = i;
+ break;
+ }
+ }
+ }
+
+ /*
+ * No match? Then restore what we were working on, but make sure
+ * the cursor is at the beginning of the line.
+ */
+ if (idx == -1) {
+ setCursorPosition(0);
+ killLine();
+ putString(origBuffer.buffer);
+ setCursorPosition(0);
+ return -1;
+ }
+
+ /*
+ * Show the match.
+ */
+ setCursorPosition(0);
+ killLine();
+ putString(history.get(idx));
+ setCursorPosition(0);
+ flush();
+
+ /*
+ * While searching really only the "n" and "N" keys are interpreted
+ * as movement, any other key is treated as if you are editing the
+ * line with it, so we return it back up to the caller for interpretation.
+ */
+ isComplete = false;
+ while (!isComplete && (ch = readCharacter()) != -1) {
+ boolean forward = isForward;
+ switch (ch) {
+ case 'p': case 'P':
+ forward = !isForward;
+ // Fallthru
+ case 'n': case 'N':
+ boolean isMatch = false;
+ if (forward) {
+ for (int i = idx+1; !isMatch && i < end; i++) {
+ if (history.get(i).toString().contains(searchTerm)) {
+ idx = i;
+ isMatch = true;
+ }
+ }
+ }
+ else {
+ for (int i = idx - 1; !isMatch && i >= start; i--) {
+ if (history.get(i).toString().contains(searchTerm)) {
+ idx = i;
+ isMatch = true;
+ }
+ }
+ }
+ if (isMatch) {
+ setCursorPosition(0);
+ killLine();
+ putString(history.get(idx));
+ setCursorPosition(0);
+ }
+ break;
+ default:
+ isComplete = true;
+ }
+ flush();
+ }
+
+ /*
+ * Complete?
+ */
+ return ch;
+ }
+
+ public void setParenBlinkTimeout(int timeout) {
+ parenBlinkTimeout = timeout;
+ }
+
+ private void insertClose(String s) throws IOException {
+ putString(s);
+ int closePosition = buf.cursor;
+
+ moveCursor(-1);
+ viMatch();
+
+
+ if (in.isNonBlockingEnabled()) {
+ in.peek(parenBlinkTimeout);
+ }
+
+ setCursorPosition(closePosition);
+ }
+
+ /**
+ * Implements vi style bracket matching ("%" command). The matching
+ * bracket for the current bracket type that you are sitting on is matched.
+ * The logic works like so:
+ * @return true if it worked, false if the cursor was not on a bracket
+ * character or if there was no matching bracket.
+ * @throws IOException
+ */
+ private boolean viMatch() throws IOException {
+ int pos = buf.cursor;
+
+ if (pos == buf.length()) {
+ return false;
+ }
+
+ int type = getBracketType(buf.buffer.charAt (pos));
+ int move = (type < 0) ? -1 : 1;
+ int count = 1;
+
+ if (type == 0)
+ return false;
+
+ while (count > 0) {
+ pos += move;
+
+ // Fell off the start or end.
+ if (pos < 0 || pos >= buf.buffer.length ()) {
+ return false;
+ }
+
+ int curType = getBracketType(buf.buffer.charAt (pos));
+ if (curType == type) {
+ ++count;
+ }
+ else if (curType == -type) {
+ --count;
+ }
+ }
+
+ /*
+ * Slight adjustment for delete-to, yank-to, change-to to ensure
+ * that the matching paren is consumed
+ */
+ if (move > 0 && isInViMoveOperationState())
+ ++pos;
+
+ setCursorPosition(pos);
+ return true;
+ }
+
+ /**
+ * Given a character determines what type of bracket it is (paren,
+ * square, curly, or none).
+ * @param ch The character to check
+ * @return 1 is square, 2 curly, 3 parent, or zero for none. The value
+ * will be negated if it is the closing form of the bracket.
+ */
+ private int getBracketType (char ch) {
+ switch (ch) {
+ case '[': return 1;
+ case ']': return -1;
+ case '{': return 2;
+ case '}': return -2;
+ case '(': return 3;
+ case ')': return -3;
+ default:
+ return 0;
+ }
+ }
+
+ private boolean deletePreviousWord() throws IOException {
+ StringBuilder killed = new StringBuilder();
+ char c;
+
+ while (isDelimiter((c = buf.current()))) {
+ if (c == 0) {
+ break;
+ }
+
+ killed.append(c);
+ backspace();
+ }
+
+ while (!isDelimiter((c = buf.current()))) {
+ if (c == 0) {
+ break;
+ }
+
+ killed.append(c);
+ backspace();
+ }
+
+ String copy = killed.reverse().toString();
+ killRing.addBackwards(copy);
+ return true;
+ }
+
+ private boolean deleteNextWord() throws IOException {
+ StringBuilder killed = new StringBuilder();
+ char c;
+
+ while (isDelimiter((c = buf.nextChar()))) {
+ if (c == 0) {
+ break;
+ }
+ killed.append(c);
+ delete();
+ }
+
+ while (!isDelimiter((c = buf.nextChar()))) {
+ if (c == 0) {
+ break;
+ }
+ killed.append(c);
+ delete();
+ }
+
+ String copy = killed.toString();
+ killRing.add(copy);
+
+ return true;
+ }
+
+ private boolean capitalizeWord() throws IOException {
+ boolean first = true;
+ int i = 1;
+ char c;
+ while (buf.cursor + i - 1< buf.length() && !isDelimiter((c = buf.buffer.charAt(buf.cursor + i - 1)))) {
+ buf.buffer.setCharAt(buf.cursor + i - 1, first ? Character.toUpperCase(c) : Character.toLowerCase(c));
+ first = false;
+ i++;
+ }
+ drawBuffer();
+ moveCursor(i - 1);
+ return true;
+ }
+
+ private boolean upCaseWord() throws IOException {
+ int i = 1;
+ char c;
+ while (buf.cursor + i - 1 < buf.length() && !isDelimiter((c = buf.buffer.charAt(buf.cursor + i - 1)))) {
+ buf.buffer.setCharAt(buf.cursor + i - 1, Character.toUpperCase(c));
+ i++;
+ }
+ drawBuffer();
+ moveCursor(i - 1);
+ return true;
+ }
+
+ private boolean downCaseWord() throws IOException {
+ int i = 1;
+ char c;
+ while (buf.cursor + i - 1 < buf.length() && !isDelimiter((c = buf.buffer.charAt(buf.cursor + i - 1)))) {
+ buf.buffer.setCharAt(buf.cursor + i - 1, Character.toLowerCase(c));
+ i++;
+ }
+ drawBuffer();
+ moveCursor(i - 1);
+ return true;
+ }
+
+ /**
+ * Performs character transpose. The character prior to the cursor and the
+ * character under the cursor are swapped and the cursor is advanced one
+ * character unless you are already at the end of the line.
+ *
+ * @param count The number of times to perform the transpose
+ * @return true if the operation succeeded, false otherwise (e.g. transpose
+ * cannot happen at the beginning of the line).
+ * @throws IOException
+ */
+ private boolean transposeChars(int count) throws IOException {
+ for (; count > 0; --count) {
+ if (buf.cursor == 0 || buf.cursor == buf.buffer.length()) {
+ return false;
+ }
+
+ int first = buf.cursor-1;
+ int second = buf.cursor;
+
+ char tmp = buf.buffer.charAt (first);
+ buf.buffer.setCharAt(first, buf.buffer.charAt(second));
+ buf.buffer.setCharAt(second, tmp);
+
+ // This could be done more efficiently by only re-drawing at the end.
+ moveInternal(-1);
+ drawBuffer();
+ moveInternal(2);
+ }
+
+ return true;
+ }
+
+ public boolean isKeyMap(String name) {
+ // Current keymap.
+ KeyMap map = consoleKeys.getKeys();
+ KeyMap mapByName = consoleKeys.getKeyMaps().get(name);
+
+ if (mapByName == null)
+ return false;
+
+ /*
+ * This may not be safe to do, but there doesn't appear to be a
+ * clean way to find this information out.
+ */
+ return map == mapByName;
+ }
+
+
+ /**
+ * The equivalent of hitting <RET>. The line is considered
+ * complete and is returned.
+ *
+ * @return The completed line of text.
+ * @throws IOException
+ */
+ public String accept() throws IOException {
+ moveToEnd();
+ println(); // output newline
+ flush();
+ return finishBuffer();
+ }
+
+ private void abort() throws IOException {
+ beep();
+ buf.clear();
+ println();
+ redrawLine();
+ }
+
+ /**
+ * Move the cursor <i>where</i> characters.
+ *
+ * @param num If less than 0, move abs(<i>where</i>) to the left, otherwise move <i>where</i> to the right.
+ * @return The number of spaces we moved
+ */
+ public int moveCursor(final int num) throws IOException {
+ int where = num;
+
+ if ((buf.cursor == 0) && (where <= 0)) {
+ return 0;
+ }
+
+ if ((buf.cursor == buf.buffer.length()) && (where >= 0)) {
+ return 0;
+ }
+
+ if ((buf.cursor + where) < 0) {
+ where = -buf.cursor;
+ }
+ else if ((buf.cursor + where) > buf.buffer.length()) {
+ where = buf.buffer.length() - buf.cursor;
+ }
+
+ moveInternal(where);
+
+ return where;
+ }
+
+ /**
+ * Move the cursor <i>where</i> characters, without checking the current buffer.
+ *
+ * @param where the number of characters to move to the right or left.
+ */
+ private void moveInternal(final int where) throws IOException {
+ // debug ("move cursor " + where + " ("
+ // + buf.cursor + " => " + (buf.cursor + where) + ")");
+ buf.cursor += where;
+
+ if (terminal.isAnsiSupported()) {
+ if (where < 0) {
+ back(Math.abs(where));
+ } else {
+ int width = getTerminal().getWidth();
+ int cursor = getCursorPosition();
+ int oldLine = (cursor - where) / width;
+ int newLine = cursor / width;
+ if (newLine > oldLine) {
+ printAnsiSequence((newLine - oldLine) + "B");
+ }
+ printAnsiSequence(1 +(cursor % width) + "G");
+ }
+// flush();
+ return;
+ }
+
+ char c;
+
+ if (where < 0) {
+ int len = 0;
+ for (int i = buf.cursor; i < buf.cursor - where; i++) {
+ if (buf.buffer.charAt(i) == '\t') {
+ len += TAB_WIDTH;
+ }
+ else {
+ len++;
+ }
+ }
+
+ char chars[] = new char[len];
+ Arrays.fill(chars, BACKSPACE);
+ out.write(chars);
+
+ return;
+ }
+ else if (buf.cursor == 0) {
+ return;
+ }
+ else if (mask != null) {
+ c = mask;
+ }
+ else {
+ print(buf.buffer.substring(buf.cursor - where, buf.cursor).toCharArray());
+ return;
+ }
+
+ // null character mask: don't output anything
+ if (mask == NULL_MASK) {
+ return;
+ }
+
+ print(c, Math.abs(where));
+ }
+
+ // FIXME: replace() is not used
+
+ public final boolean replace(final int num, final String replacement) {
+ buf.buffer.replace(buf.cursor - num, buf.cursor, replacement);
+ try {
+ moveCursor(-num);
+ drawBuffer(Math.max(0, num - replacement.length()));
+ moveCursor(replacement.length());
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Read a character from the console.
+ *
+ * @return the character, or -1 if an EOF is received.
+ */
+ public final int readCharacter() throws IOException {
+ int c = reader.read();
+ if (c >= 0) {
+ Log.trace("Keystroke: ", c);
+ // clear any echo characters
+ if (terminal.isSupported()) {
+ clearEcho(c);
+ }
+ }
+ return c;
+ }
+
+ /**
+ * Clear the echoed characters for the specified character code.
+ */
+ private int clearEcho(final int c) throws IOException {
+ // if the terminal is not echoing, then ignore
+ if (!terminal.isEchoEnabled()) {
+ return 0;
+ }
+
+ // otherwise, clear
+ int num = countEchoCharacters(c);
+ back(num);
+ drawBuffer(num);
+
+ return num;
+ }
+
+ private int countEchoCharacters(final int c) {
+ // tabs as special: we need to determine the number of spaces
+ // to cancel based on what out current cursor position is
+ if (c == 9) {
+ int tabStop = 8; // will this ever be different?
+ int position = getCursorPosition();
+
+ return tabStop - (position % tabStop);
+ }
+
+ return getPrintableCharacters(c).length();
+ }
+
+ /**
+ * Return the number of characters that will be printed when the specified
+ * character is echoed to the screen
+ *
+ * Adapted from cat by Torbjorn Granlund, as repeated in stty by David MacKenzie.
+ */
+ private StringBuilder getPrintableCharacters(final int ch) {
+ StringBuilder sbuff = new StringBuilder();
+
+ if (ch >= 32) {
+ if (ch < 127) {
+ sbuff.append(ch);
+ }
+ else if (ch == 127) {
+ sbuff.append('^');
+ sbuff.append('?');
+ }
+ else {
+ sbuff.append('M');
+ sbuff.append('-');
+
+ if (ch >= (128 + 32)) {
+ if (ch < (128 + 127)) {
+ sbuff.append((char) (ch - 128));
+ }
+ else {
+ sbuff.append('^');
+ sbuff.append('?');
+ }
+ }
+ else {
+ sbuff.append('^');
+ sbuff.append((char) (ch - 128 + 64));
+ }
+ }
+ }
+ else {
+ sbuff.append('^');
+ sbuff.append((char) (ch + 64));
+ }
+
+ return sbuff;
+ }
+
+ public final int readCharacter(final char... allowed) throws IOException {
+ // if we restrict to a limited set and the current character is not in the set, then try again.
+ char c;
+
+ Arrays.sort(allowed); // always need to sort before binarySearch
+
+ while (Arrays.binarySearch(allowed, c = (char) readCharacter()) < 0) {
+ // nothing
+ }
+
+ return c;
+ }
+
+ //
+ // Key Bindings
+ //
+
+ public static final String JLINE_COMPLETION_THRESHOLD = "jline.completion.threshold";
+
+ //
+ // Line Reading
+ //
+
+ /**
+ * Read the next line and return the contents of the buffer.
+ */
+ public String readLine() throws IOException {
+ return readLine((String) null);
+ }
+
+ /**
+ * Read the next line with the specified character mask. If null, then
+ * characters will be echoed. If 0, then no characters will be echoed.
+ */
+ public String readLine(final Character mask) throws IOException {
+ return readLine(null, mask);
+ }
+
+ public String readLine(final String prompt) throws IOException {
+ return readLine(prompt, null);
+ }
+
+ /**
+ * Sets the current keymap by name. Supported keymaps are "emacs",
+ * "vi-insert", "vi-move".
+ * @param name The name of the keymap to switch to
+ * @return true if the keymap was set, or false if the keymap is
+ * not recognized.
+ */
+ public boolean setKeyMap(String name) {
+ return consoleKeys.setKeyMap(name);
+ }
+
+ /**
+ * Returns the name of the current key mapping.
+ * @return the name of the key mapping. This will be the canonical name
+ * of the current mode of the key map and may not reflect the name that
+ * was used with {@link #setKeyMap(String)}.
+ */
+ public String getKeyMap() {
+ return consoleKeys.getKeys().getName();
+ }
+
+ /**
+ * Read a line from the <i>in</i> {@link InputStream}, and return the line
+ * (without any trailing newlines).
+ *
+ * @param prompt The prompt to issue to the console, may be null.
+ * @return A line that is read from the terminal, or null if there was null input (e.g., <i>CTRL-D</i>
+ * was pressed).
+ */
+ public String readLine(String prompt, final Character mask) throws IOException {
+ // prompt may be null
+ // mask may be null
+
+ /*
+ * This is the accumulator for VI-mode repeat count. That is, while in
+ * move mode, if you type 30x it will delete 30 characters. This is
+ * where the "30" is accumulated until the command is struck.
+ */
+ int repeatCount = 0;
+
+ // FIXME: This blows, each call to readLine will reset the console's state which doesn't seem very nice.
+ this.mask = mask;
+ if (prompt != null) {
+ setPrompt(prompt);
+ }
+ else {
+ prompt = getPrompt();
+ }
+
+ try {
+ if (!terminal.isSupported()) {
+ beforeReadLine(prompt, mask);
+ }
+
+ if (prompt != null && prompt.length() > 0) {
+ out.write(prompt);
+ out.flush();
+ }
+
+ // if the terminal is unsupported, just use plain-java reading
+ if (!terminal.isSupported()) {
+ return readLineSimple();
+ }
+
+ if (handleUserInterrupt && (terminal instanceof UnixTerminal)) {
+ ((UnixTerminal) terminal).disableInterruptCharacter();
+ }
+
+ String originalPrompt = this.prompt;
+
+ state = State.NORMAL;
+
+ boolean success = true;
+
+ StringBuilder sb = new StringBuilder();
+ Stack<Character> pushBackChar = new Stack<Character>();
+ while (true) {
+ int c = pushBackChar.isEmpty() ? readCharacter() : pushBackChar.pop ();
+ if (c == -1) {
+ return null;
+ }
+ sb.appendCodePoint(c);
+
+ if (recording) {
+ macro += new String(new int[]{c}, 0, 1);
+ }
+
+ Object o = getKeys().getBound( sb );
+ /*
+ * The kill ring keeps record of whether or not the
+ * previous command was a yank or a kill. We reset
+ * that state here if needed.
+ */
+ if (!recording && !(o instanceof KeyMap)) {
+ if (o != Operation.YANK_POP && o != Operation.YANK) {
+ killRing.resetLastYank();
+ }
+ if (o != Operation.KILL_LINE && o != Operation.KILL_WHOLE_LINE
+ && o != Operation.BACKWARD_KILL_WORD && o != Operation.KILL_WORD
+ && o != Operation.UNIX_LINE_DISCARD && o != Operation.UNIX_WORD_RUBOUT) {
+ killRing.resetLastKill();
+ }
+ }
+
+ if (o == Operation.DO_LOWERCASE_VERSION) {
+ sb.setLength( sb.length() - 1);
+ sb.append( Character.toLowerCase( (char) c ));
+ o = getKeys().getBound( sb );
+ }
+
+ /*
+ * A KeyMap indicates that the key that was struck has a
+ * number of keys that can follow it as indicated in the
+ * map. This is used primarily for Emacs style ESC-META-x
+ * lookups. Since more keys must follow, go back to waiting
+ * for the next key.
+ */
+ if ( o instanceof KeyMap ) {
+ /*
+ * The ESC key (#27) is special in that it is ambiguous until
+ * you know what is coming next. The ESC could be a literal
+ * escape, like the user entering vi-move mode, or it could
+ * be part of a terminal control sequence. The following
+ * logic attempts to disambiguate things in the same
+ * fashion as regular vi or readline.
+ *
+ * When ESC is encountered and there is no other pending
+ * character in the pushback queue, then attempt to peek
+ * into the input stream (if the feature is enabled) for
+ * 150ms. If nothing else is coming, then assume it is
+ * not a terminal control sequence, but a raw escape.
+ */
+ if (c == 27
+ && pushBackChar.isEmpty()
+ && in.isNonBlockingEnabled()
+ && in.peek(escapeTimeout) == -2) {
+ o = ((KeyMap) o).getAnotherKey();
+ if (o == null || o instanceof KeyMap) {
+ continue;
+ }
+ sb.setLength(0);
+ }
+ else {
+ continue;
+ }
+ }
+
+ /*
+ * If we didn't find a binding for the key and there is
+ * more than one character accumulated then start checking
+ * the largest span of characters from the beginning to
+ * see if there is a binding for them.
+ *
+ * For example if our buffer has ESC,CTRL-M,C the getBound()
+ * called previously indicated that there is no binding for
+ * this sequence, so this then checks ESC,CTRL-M, and failing
+ * that, just ESC. Each keystroke that is pealed off the end
+ * during these tests is stuffed onto the pushback buffer so
+ * they won't be lost.
+ *
+ * If there is no binding found, then we go back to waiting for
+ * input.
+ */
+ while ( o == null && sb.length() > 0 ) {
+ c = sb.charAt( sb.length() - 1 );
+ sb.setLength( sb.length() - 1 );
+ Object o2 = getKeys().getBound( sb );
+ if ( o2 instanceof KeyMap ) {
+ o = ((KeyMap) o2).getAnotherKey();
+ if ( o == null ) {
+ continue;
+ } else {
+ pushBackChar.push( (char) c );
+ }
+ }
+ }
+
+ if ( o == null ) {
+ continue;
+ }
+ Log.trace("Binding: ", o);
+
+
+ // Handle macros
+ if (o instanceof String) {
+ String macro = (String) o;
+ for (int i = 0; i < macro.length(); i++) {
+ pushBackChar.push(macro.charAt(macro.length() - 1 - i));
+ }
+ sb.setLength( 0 );
+ continue;
+ }
+
+ // Handle custom callbacks
+ //original code:
+// if (o instanceof ActionListener) {
+// ((ActionListener) o).actionPerformed(null);
+// sb.setLength( 0 );
+// continue;
+// }
+ //using reflection to avoid dependency on java.desktop:
+ try {
+ Class<?> actionListener =
+ Class.forName("java.awt.event.ActionListener", false, ClassLoader.getSystemClassLoader());
+ Class<?> actionEvent =
+ Class.forName("java.awt.event.ActionEvent", false, ClassLoader.getSystemClassLoader());
+ if (actionListener.isAssignableFrom(o.getClass())) {
+ Method actionPerformed =
+ actionListener.getMethod("actionPerformed", actionEvent);
+ try {
+ actionPerformed.invoke(o, (Object) null);
+ } catch (InvocationTargetException ex ) {
+ Log.error("Exception while running registered action", ex);
+ }
+ sb.setLength( 0 );
+ continue;
+ }
+ } catch (ReflectiveOperationException ex) {
+ //ignore
+ }
+
+ // Search mode.
+ //
+ // Note that we have to do this first, because if there is a command
+ // not linked to a search command, we leave the search mode and fall
+ // through to the normal state.
+ if (state == State.SEARCH || state == State.FORWARD_SEARCH) {
+ int cursorDest = -1;
+ switch ( ((Operation) o )) {
+ case ABORT:
+ state = State.NORMAL;
+ buf.clear();
+ buf.buffer.append(searchTerm);
+ break;
+
+ case REVERSE_SEARCH_HISTORY:
+ state = State.SEARCH;
+ if (searchTerm.length() == 0) {
+ searchTerm.append(previousSearchTerm);
+ }
+
+ if (searchIndex > 0) {
+ searchIndex = searchBackwards(searchTerm.toString(), searchIndex);
+ }
+ break;
+
+ case FORWARD_SEARCH_HISTORY:
+ state = State.FORWARD_SEARCH;
+ if (searchTerm.length() == 0) {
+ searchTerm.append(previousSearchTerm);
+ }
+
+ if (searchIndex > -1 && searchIndex < history.size() - 1) {
+ searchIndex = searchForwards(searchTerm.toString(), searchIndex);
+ }
+ break;
+
+ case BACKWARD_DELETE_CHAR:
+ if (searchTerm.length() > 0) {
+ searchTerm.deleteCharAt(searchTerm.length() - 1);
+ if (state == State.SEARCH) {
+ searchIndex = searchBackwards(searchTerm.toString());
+ } else {
+ searchIndex = searchForwards(searchTerm.toString());
+ }
+ }
+ break;
+
+ case SELF_INSERT:
+ searchTerm.appendCodePoint(c);
+ if (state == State.SEARCH) {
+ searchIndex = searchBackwards(searchTerm.toString());
+ } else {
+ searchIndex = searchForwards(searchTerm.toString());
+ }
+ break;
+
+ default:
+ // Set buffer and cursor position to the found string.
+ if (searchIndex != -1) {
+ history.moveTo(searchIndex);
+ // set cursor position to the found string
+ cursorDest = history.current().toString().indexOf(searchTerm.toString());
+ }
+ state = State.NORMAL;
+ break;
+ }
+
+ // if we're still in search mode, print the search status
+ if (state == State.SEARCH || state == State.FORWARD_SEARCH) {
+ if (searchTerm.length() == 0) {
+ if (state == State.SEARCH) {
+ printSearchStatus("", "");
+ } else {
+ printForwardSearchStatus("", "");
+ }
+ searchIndex = -1;
+ } else {
+ if (searchIndex == -1) {
+ beep();
+ printSearchStatus(searchTerm.toString(), "");
+ } else if (state == State.SEARCH) {
+ printSearchStatus(searchTerm.toString(), history.get(searchIndex).toString());
+ } else {
+ printForwardSearchStatus(searchTerm.toString(), history.get(searchIndex).toString());
+ }
+ }
+ }
+ // otherwise, restore the line
+ else {
+ restoreLine(originalPrompt, cursorDest);
+ }
+ }
+ if (state != State.SEARCH && state != State.FORWARD_SEARCH) {
+ /*
+ * If this is still false at the end of the switch, then
+ * we reset our repeatCount to 0.
+ */
+ boolean isArgDigit = false;
+
+ /*
+ * Every command that can be repeated a specified number
+ * of times, needs to know how many times to repeat, so
+ * we figure that out here.
+ */
+ int count = (repeatCount == 0) ? 1 : repeatCount;
+
+ /*
+ * Default success to true. You only need to explicitly
+ * set it if something goes wrong.
+ */
+ success = true;
+
+ if (o instanceof Operation) {
+ Operation op = (Operation)o;
+ /*
+ * Current location of the cursor (prior to the operation).
+ * These are used by vi *-to operation (e.g. delete-to)
+ * so we know where we came from.
+ */
+ int cursorStart = buf.cursor;
+ State origState = state;
+
+ /*
+ * If we are on a "vi" movement based operation, then we
+ * need to restrict the sets of inputs pretty heavily.
+ */
+ if (state == State.VI_CHANGE_TO
+ || state == State.VI_YANK_TO
+ || state == State.VI_DELETE_TO) {
+
+ op = viDeleteChangeYankToRemap(op);
+ }
+
+ switch ( op ) {
+ case COMPLETE: // tab
+ // There is an annoyance with tab completion in that
+ // sometimes the user is actually pasting input in that
+ // has physical tabs in it. This attempts to look at how
+ // quickly a character follows the tab, if the character
+ // follows *immediately*, we assume it is a tab literal.
+ boolean isTabLiteral = false;
+ if (copyPasteDetection
+ && c == 9
+ && (!pushBackChar.isEmpty()
+ || (in.isNonBlockingEnabled() && in.peek(escapeTimeout) != -2))) {
+ isTabLiteral = true;
+ }
+
+ if (! isTabLiteral) {
+ success = complete();
+ }
+ else {
+ putString(sb);
+ }
+ break;
+
+ case POSSIBLE_COMPLETIONS:
+ printCompletionCandidates();
+ break;
+
+ case BEGINNING_OF_LINE:
+ success = setCursorPosition(0);
+ break;
+
+ case YANK:
+ success = yank();
+ break;
+
+ case YANK_POP:
+ success = yankPop();
+ break;
+
+ case KILL_LINE: // CTRL-K
+ success = killLine();
+ break;
+
+ case KILL_WHOLE_LINE:
+ success = setCursorPosition(0) && killLine();
+ break;
+
+ case CLEAR_SCREEN: // CTRL-L
+ success = clearScreen();
+ redrawLine();
+ break;
+
+ case OVERWRITE_MODE:
+ buf.setOverTyping(!buf.isOverTyping());
+ break;
+
+ case SELF_INSERT:
+ putString(sb);
+ break;
+
+ case ACCEPT_LINE:
+ return accept();
+
+ case ABORT:
+ if (searchTerm == null) {
+ abort();
+ }
+ break;
+
+ case INTERRUPT:
+ if (handleUserInterrupt) {
+ println();
+ flush();
+ String partialLine = buf.buffer.toString();
+ buf.clear();
+ history.moveToEnd();
+ throw new UserInterruptException(partialLine);
+ }
+ break;
+
+ /*
+ * VI_MOVE_ACCEPT_LINE is the result of an ENTER
+ * while in move mode. This is the same as a normal
+ * ACCEPT_LINE, except that we need to enter
+ * insert mode as well.
+ */
+ case VI_MOVE_ACCEPT_LINE:
+ consoleKeys.setKeyMap(KeyMap.VI_INSERT);
+ return accept();
+
+ case BACKWARD_WORD:
+ success = previousWord();
+ break;
+
+ case FORWARD_WORD:
+ success = nextWord();
+ break;
+
+ case PREVIOUS_HISTORY:
+ success = moveHistory(false);
+ break;
+
+ /*
+ * According to bash/readline move through history
+ * in "vi" mode will move the cursor to the
+ * start of the line. If there is no previous
+ * history, then the cursor doesn't move.
+ */
+ case VI_PREVIOUS_HISTORY:
+ success = moveHistory(false, count)
+ && setCursorPosition(0);
+ break;
+
+ case NEXT_HISTORY:
+ success = moveHistory(true);
+ break;
+
+ /*
+ * According to bash/readline move through history
+ * in "vi" mode will move the cursor to the
+ * start of the line. If there is no next history,
+ * then the cursor doesn't move.
+ */
+ case VI_NEXT_HISTORY:
+ success = moveHistory(true, count)
+ && setCursorPosition(0);
+ break;
+
+ case BACKWARD_DELETE_CHAR: // backspace
+ success = backspace();
+ break;
+
+ case EXIT_OR_DELETE_CHAR:
+ if (buf.buffer.length() == 0) {
+ return null;
+ }
+ success = deleteCurrentCharacter();
+ break;
+
+ case DELETE_CHAR: // delete
+ success = deleteCurrentCharacter();
+ break;
+
+ case BACKWARD_CHAR:
+ success = moveCursor(-(count)) != 0;
+ break;
+
+ case FORWARD_CHAR:
+ success = moveCursor(count) != 0;
+ break;
+
+ case UNIX_LINE_DISCARD:
+ success = resetLine();
+ break;
+
+ case UNIX_WORD_RUBOUT:
+ success = unixWordRubout(count);
+ break;
+
+ case BACKWARD_KILL_WORD:
+ success = deletePreviousWord();
+ break;
+
+ case KILL_WORD:
+ success = deleteNextWord();
+ break;
+
+ case BEGINNING_OF_HISTORY:
+ success = history.moveToFirst();
+ if (success) {
+ setBuffer(history.current());
+ }
+ break;
+
+ case END_OF_HISTORY:
+ success = history.moveToLast();
+ if (success) {
+ setBuffer(history.current());
+ }
+ break;
+
+ case HISTORY_SEARCH_BACKWARD:
+ searchTerm = new StringBuffer(buf.upToCursor());
+ searchIndex = searchBackwards(searchTerm.toString(), history.index(), true);
+
+ if (searchIndex == -1) {
+ beep();
+ } else {
+ // Maintain cursor position while searching.
+ success = history.moveTo(searchIndex);
+ if (success) {
+ setBufferKeepPos(history.current());
+ }
+ }
+ break;
+
+ case HISTORY_SEARCH_FORWARD:
+ searchTerm = new StringBuffer(buf.upToCursor());
+ int index = history.index() + 1;
+
+ if (index == history.size()) {
+ history.moveToEnd();
+ setBufferKeepPos(searchTerm.toString());
+ } else if (index < history.size()) {
+ searchIndex = searchForwards(searchTerm.toString(), index, true);
+ if (searchIndex == -1) {
+ beep();
+ } else {
+ // Maintain cursor position while searching.
+ success = history.moveTo(searchIndex);
+ if (success) {
+ setBufferKeepPos(history.current());
+ }
+ }
+ }
+ break;
+
+ case REVERSE_SEARCH_HISTORY:
+ if (searchTerm != null) {
+ previousSearchTerm = searchTerm.toString();
+ }
+ searchTerm = new StringBuffer(buf.buffer);
+ state = State.SEARCH;
+ if (searchTerm.length() > 0) {
+ searchIndex = searchBackwards(searchTerm.toString());
+ if (searchIndex == -1) {
+ beep();
+ }
+ printSearchStatus(searchTerm.toString(),
+ searchIndex > -1 ? history.get(searchIndex).toString() : "");
+ } else {
+ searchIndex = -1;
+ printSearchStatus("", "");
+ }
+ break;
+
+ case FORWARD_SEARCH_HISTORY:
+ if (searchTerm != null) {
+ previousSearchTerm = searchTerm.toString();
+ }
+ searchTerm = new StringBuffer(buf.buffer);
+ state = State.FORWARD_SEARCH;
+ if (searchTerm.length() > 0) {
+ searchIndex = searchForwards(searchTerm.toString());
+ if (searchIndex == -1) {
+ beep();
+ }
+ printForwardSearchStatus(searchTerm.toString(),
+ searchIndex > -1 ? history.get(searchIndex).toString() : "");
+ } else {
+ searchIndex = -1;
+ printForwardSearchStatus("", "");
+ }
+ break;
+
+ case CAPITALIZE_WORD:
+ success = capitalizeWord();
+ break;
+
+ case UPCASE_WORD:
+ success = upCaseWord();
+ break;
+
+ case DOWNCASE_WORD:
+ success = downCaseWord();
+ break;
+
+ case END_OF_LINE:
+ success = moveToEnd();
+ break;
+
+ case TAB_INSERT:
+ putString( "\t" );
+ break;
+
+ case RE_READ_INIT_FILE:
+ consoleKeys.loadKeys(appName, inputrcUrl);
+ break;
+
+ case START_KBD_MACRO:
+ recording = true;
+ break;
+
+ case END_KBD_MACRO:
+ recording = false;
+ macro = macro.substring(0, macro.length() - sb.length());
+ break;
+
+ case CALL_LAST_KBD_MACRO:
+ for (int i = 0; i < macro.length(); i++) {
+ pushBackChar.push(macro.charAt(macro.length() - 1 - i));
+ }
+ sb.setLength( 0 );
+ break;
+
+ case VI_EDITING_MODE:
+ consoleKeys.setKeyMap(KeyMap.VI_INSERT);
+ break;
+
+ case VI_MOVEMENT_MODE:
+ /*
+ * If we are re-entering move mode from an
+ * aborted yank-to, delete-to, change-to then
+ * don't move the cursor back. The cursor is
+ * only move on an expclit entry to movement
+ * mode.
+ */
+ if (state == State.NORMAL) {
+ moveCursor(-1);
+ }
+ consoleKeys.setKeyMap(KeyMap.VI_MOVE);
+ break;
+
+ case VI_INSERTION_MODE:
+ consoleKeys.setKeyMap(KeyMap.VI_INSERT);
+ break;
+
+ case VI_APPEND_MODE:
+ moveCursor(1);
+ consoleKeys.setKeyMap(KeyMap.VI_INSERT);
+ break;
+
+ case VI_APPEND_EOL:
+ success = moveToEnd();
+ consoleKeys.setKeyMap(KeyMap.VI_INSERT);
+ break;
+
+ /*
+ * Handler for CTRL-D. Attempts to follow readline
+ * behavior. If the line is empty, then it is an EOF
+ * otherwise it is as if the user hit enter.
+ */
+ case VI_EOF_MAYBE:
+ if (buf.buffer.length() == 0) {
+ return null;
+ }
+ return accept();
+
+ case TRANSPOSE_CHARS:
+ success = transposeChars(count);
+ break;
+
+ case INSERT_COMMENT:
+ return insertComment (false);
+
+ case INSERT_CLOSE_CURLY:
+ insertClose("}");
+ break;
+
+ case INSERT_CLOSE_PAREN:
+ insertClose(")");
+ break;
+
+ case INSERT_CLOSE_SQUARE:
+ insertClose("]");
+ break;
+
+ case VI_INSERT_COMMENT:
+ return insertComment (true);
+
+ case VI_MATCH:
+ success = viMatch ();
+ break;
+
+ case VI_SEARCH:
+ int lastChar = viSearch(sb.charAt (0));
+ if (lastChar != -1) {
+ pushBackChar.push((char)lastChar);
+ }
+ break;
+
+ case VI_ARG_DIGIT:
+ repeatCount = (repeatCount * 10) + sb.charAt(0) - '0';
+ isArgDigit = true;
+ break;
+
+ case VI_BEGNNING_OF_LINE_OR_ARG_DIGIT:
+ if (repeatCount > 0) {
+ repeatCount = (repeatCount * 10) + sb.charAt(0) - '0';
+ isArgDigit = true;
+ }
+ else {
+ success = setCursorPosition(0);
+ }
+ break;
+
+ case VI_FIRST_PRINT:
+ success = setCursorPosition(0) && viNextWord(1);
+ break;
+
+ case VI_PREV_WORD:
+ success = viPreviousWord(count);
+ break;
+
+ case VI_NEXT_WORD:
+ success = viNextWord(count);
+ break;
+
+ case VI_END_WORD:
+ success = viEndWord(count);
+ break;
+
+ case VI_INSERT_BEG:
+ success = setCursorPosition(0);
+ consoleKeys.setKeyMap(KeyMap.VI_INSERT);
+ break;
+
+ case VI_RUBOUT:
+ success = viRubout(count);
+ break;
+
+ case VI_DELETE:
+ success = viDelete(count);
+ break;
+
+ case VI_DELETE_TO:
+ /*
+ * This is a weird special case. In vi
+ * "dd" deletes the current line. So if we
+ * get a delete-to, followed by a delete-to,
+ * we delete the line.
+ */
+ if (state == State.VI_DELETE_TO) {
+ success = setCursorPosition(0) && killLine();
+ state = origState = State.NORMAL;
+ }
+ else {
+ state = State.VI_DELETE_TO;
+ }
+ break;
+
+ case VI_YANK_TO:
+ // Similar to delete-to, a "yy" yanks the whole line.
+ if (state == State.VI_YANK_TO) {
+ yankBuffer = buf.buffer.toString();
+ state = origState = State.NORMAL;
+ }
+ else {
+ state = State.VI_YANK_TO;
+ }
+ break;
+
+ case VI_CHANGE_TO:
+ if (state == State.VI_CHANGE_TO) {
+ success = setCursorPosition(0) && killLine();
+ state = origState = State.NORMAL;
+ consoleKeys.setKeyMap(KeyMap.VI_INSERT);
+ }
+ else {
+ state = State.VI_CHANGE_TO;
+ }
+ break;
+
+ case VI_KILL_WHOLE_LINE:
+ success = setCursorPosition(0) && killLine();
+ consoleKeys.setKeyMap(KeyMap.VI_INSERT);
+ break;
+
+ case VI_PUT:
+ success = viPut(count);
+ break;
+
+ case VI_CHAR_SEARCH: {
+ // ';' and ',' don't need another character. They indicate repeat next or repeat prev.
+ int searchChar = (c != ';' && c != ',')
+ ? (pushBackChar.isEmpty()
+ ? readCharacter()
+ : pushBackChar.pop ())
+ : 0;
+
+ success = viCharSearch(count, c, searchChar);
+ }
+ break;
+
+ case VI_CHANGE_CASE:
+ success = viChangeCase(count);
+ break;
+
+ case VI_CHANGE_CHAR:
+ success = viChangeChar(count,
+ pushBackChar.isEmpty()
+ ? readCharacter()
+ : pushBackChar.pop());
+ break;
+
+ case VI_DELETE_TO_EOL:
+ success = viDeleteTo(buf.cursor, buf.buffer.length(), false);
+ break;
+
+ case VI_CHANGE_TO_EOL:
+ success = viDeleteTo(buf.cursor, buf.buffer.length(), true);
+ consoleKeys.setKeyMap(KeyMap.VI_INSERT);
+ break;
+
+ case EMACS_EDITING_MODE:
+ consoleKeys.setKeyMap(KeyMap.EMACS);
+ break;
+
+ default:
+ break;
+ }
+
+ /*
+ * If we were in a yank-to, delete-to, move-to
+ * when this operation started, then fall back to
+ */
+ if (origState != State.NORMAL) {
+ if (origState == State.VI_DELETE_TO) {
+ success = viDeleteTo(cursorStart, buf.cursor, false);
+ }
+ else if (origState == State.VI_CHANGE_TO) {
+ success = viDeleteTo(cursorStart, buf.cursor, true);
+ consoleKeys.setKeyMap(KeyMap.VI_INSERT);
+ }
+ else if (origState == State.VI_YANK_TO) {
+ success = viYankTo(cursorStart, buf.cursor);
+ }
+ state = State.NORMAL;
+ }
+
+ /*
+ * Another subtly. The check for the NORMAL state is
+ * to ensure that we do not clear out the repeat
+ * count when in delete-to, yank-to, or move-to modes.
+ */
+ if (state == State.NORMAL && !isArgDigit) {
+ /*
+ * If the operation performed wasn't a vi argument
+ * digit, then clear out the current repeatCount;
+ */
+ repeatCount = 0;
+ }
+
+ if (state != State.SEARCH && state != State.FORWARD_SEARCH) {
+ previousSearchTerm = "";
+ searchTerm = null;
+ searchIndex = -1;
+ }
+ }
+ }
+ if (!success) {
+ beep();
+ }
+ sb.setLength( 0 );
+ flush();
+ }
+ }
+ finally {
+ if (!terminal.isSupported()) {
+ afterReadLine();
+ }
+ if (handleUserInterrupt && (terminal instanceof UnixTerminal)) {
+ ((UnixTerminal) terminal).enableInterruptCharacter();
+ }
+ }
+ }
+
+ /**
+ * Read a line for unsupported terminals.
+ */
+ private String readLineSimple() throws IOException {
+ StringBuilder buff = new StringBuilder();
+
+ if (skipLF) {
+ skipLF = false;
+
+ int i = readCharacter();
+
+ if (i == -1 || i == '\r') {
+ return buff.toString();
+ } else if (i == '\n') {
+ // ignore
+ } else {
+ buff.append((char) i);
+ }
+ }
+
+ while (true) {
+ int i = readCharacter();
+
+ if (i == -1 && buff.length() == 0) {
+ return null;
+ }
+
+ if (i == -1 || i == '\n') {
+ return buff.toString();
+ } else if (i == '\r') {
+ skipLF = true;
+ return buff.toString();
+ } else {
+ buff.append((char) i);
+ }
+ }
+ }
+
+ //
+ // Completion
+ //
+
+ private final List<Completer> completers = new LinkedList<Completer>();
+
+ private CompletionHandler completionHandler = new CandidateListCompletionHandler();
+
+ /**
+ * Add the specified {@link jline.console.completer.Completer} to the list of handlers for tab-completion.
+ *
+ * @param completer the {@link jline.console.completer.Completer} to add
+ * @return true if it was successfully added
+ */
+ public boolean addCompleter(final Completer completer) {
+ return completers.add(completer);
+ }
+
+ /**
+ * Remove the specified {@link jline.console.completer.Completer} from the list of handlers for tab-completion.
+ *
+ * @param completer The {@link Completer} to remove
+ * @return True if it was successfully removed
+ */
+ public boolean removeCompleter(final Completer completer) {
+ return completers.remove(completer);
+ }
+
+ /**
+ * Returns an unmodifiable list of all the completers.
+ */
+ public Collection<Completer> getCompleters() {
+ return Collections.unmodifiableList(completers);
+ }
+
+ public void setCompletionHandler(final CompletionHandler handler) {
+ this.completionHandler = checkNotNull(handler);
+ }
+
+ public CompletionHandler getCompletionHandler() {
+ return this.completionHandler;
+ }
+
+ /**
+ * Use the completers to modify the buffer with the appropriate completions.
+ *
+ * @return true if successful
+ */
+ protected boolean complete() throws IOException {
+ // debug ("tab for (" + buf + ")");
+ if (completers.size() == 0) {
+ return false;
+ }
+
+ List<CharSequence> candidates = new LinkedList<CharSequence>();
+ String bufstr = buf.buffer.toString();
+ int cursor = buf.cursor;
+
+ int position = -1;
+
+ for (Completer comp : completers) {
+ if ((position = comp.complete(bufstr, cursor, candidates)) != -1) {
+ break;
+ }
+ }
+
+ return candidates.size() != 0 && getCompletionHandler().complete(this, candidates, position);
+ }
+
+ protected void printCompletionCandidates() throws IOException {
+ // debug ("tab for (" + buf + ")");
+ if (completers.size() == 0) {
+ return;
+ }
+
+ List<CharSequence> candidates = new LinkedList<CharSequence>();
+ String bufstr = buf.buffer.toString();
+ int cursor = buf.cursor;
+
+ for (Completer comp : completers) {
+ if (comp.complete(bufstr, cursor, candidates) != -1) {
+ break;
+ }
+ }
+ CandidateListCompletionHandler.printCandidates(this, candidates);
+ drawLine();
+ }
+
+ /**
+ * The number of tab-completion candidates above which a warning will be
+ * prompted before showing all the candidates.
+ */
+ private int autoprintThreshold = Configuration.getInteger(JLINE_COMPLETION_THRESHOLD, 100); // same default as bash
+
+ /**
+ * @param threshold the number of candidates to print without issuing a warning.
+ */
+ public void setAutoprintThreshold(final int threshold) {
+ this.autoprintThreshold = threshold;
+ }
+
+ /**
+ * @return the number of candidates to print without issuing a warning.
+ */
+ public int getAutoprintThreshold() {
+ return autoprintThreshold;
+ }
+
+ private boolean paginationEnabled;
+
+ /**
+ * Whether to use pagination when the number of rows of candidates exceeds the height of the terminal.
+ */
+ public void setPaginationEnabled(final boolean enabled) {
+ this.paginationEnabled = enabled;
+ }
+
+ /**
+ * Whether to use pagination when the number of rows of candidates exceeds the height of the terminal.
+ */
+ public boolean isPaginationEnabled() {
+ return paginationEnabled;
+ }
+
+ //
+ // History
+ //
+
+ private History history = new MemoryHistory();
+
+ public void setHistory(final History history) {
+ this.history = history;
+ }
+
+ public History getHistory() {
+ return history;
+ }
+
+ private boolean historyEnabled = true;
+
+ /**
+ * Whether or not to add new commands to the history buffer.
+ */
+ public void setHistoryEnabled(final boolean enabled) {
+ this.historyEnabled = enabled;
+ }
+
+ /**
+ * Whether or not to add new commands to the history buffer.
+ */
+ public boolean isHistoryEnabled() {
+ return historyEnabled;
+ }
+
+ /**
+ * Used in "vi" mode for argumented history move, to move a specific
+ * number of history entries forward or back.
+ *
+ * @param next If true, move forward
+ * @param count The number of entries to move
+ * @return true if the move was successful
+ * @throws IOException
+ */
+ private boolean moveHistory(final boolean next, int count) throws IOException {
+ boolean ok = true;
+ for (int i = 0; i < count && (ok = moveHistory(next)); i++) {
+ /* empty */
+ }
+ return ok;
+ }
+
+ /**
+ * Move up or down the history tree.
+ */
+ private boolean moveHistory(final boolean next) throws IOException {
+ if (next && !history.next()) {
+ return false;
+ }
+ else if (!next && !history.previous()) {
+ return false;
+ }
+
+ setBuffer(history.current());
+
+ return true;
+ }
+
+ //
+ // Printing
+ //
+
+ public static final String CR = Configuration.getLineSeparator();
+
+ /**
+ * Output the specified character to the output stream without manipulating the current buffer.
+ */
+ private void print(final int c) throws IOException {
+ if (c == '\t') {
+ char chars[] = new char[TAB_WIDTH];
+ Arrays.fill(chars, ' ');
+ out.write(chars);
+ return;
+ }
+
+ out.write(c);
+ }
+
+ /**
+ * Output the specified characters to the output stream without manipulating the current buffer.
+ */
+ private void print(final char... buff) throws IOException {
+ int len = 0;
+ for (char c : buff) {
+ if (c == '\t') {
+ len += TAB_WIDTH;
+ }
+ else {
+ len++;
+ }
+ }
+
+ char chars[];
+ if (len == buff.length) {
+ chars = buff;
+ }
+ else {
+ chars = new char[len];
+ int pos = 0;
+ for (char c : buff) {
+ if (c == '\t') {
+ Arrays.fill(chars, pos, pos + TAB_WIDTH, ' ');
+ pos += TAB_WIDTH;
+ }
+ else {
+ chars[pos] = c;
+ pos++;
+ }
+ }
+ }
+
+ out.write(chars);
+ }
+
+ private void print(final char c, final int num) throws IOException {
+ if (num == 1) {
+ print(c);
+ }
+ else {
+ char[] chars = new char[num];
+ Arrays.fill(chars, c);
+ print(chars);
+ }
+ }
+
+ /**
+ * Output the specified string to the output stream (but not the buffer).
+ */
+ public final void print(final CharSequence s) throws IOException {
+ print(checkNotNull(s).toString().toCharArray());
+ }
+
+ public final void println(final CharSequence s) throws IOException {
+ print(checkNotNull(s).toString().toCharArray());
+ println();
+ }
+
+ /**
+ * Output a platform-dependant newline.
+ */
+ public final void println() throws IOException {
+ print(CR);
+// flush();
+ }
+
+ //
+ // Actions
+ //
+
+ /**
+ * Issue a delete.
+ *
+ * @return true if successful
+ */
+ public final boolean delete() throws IOException {
+ if (buf.cursor == buf.buffer.length()) {
+ return false;
+ }
+
+ buf.buffer.delete(buf.cursor, buf.cursor + 1);
+ drawBuffer(1);
+
+ return true;
+ }
+
+ /**
+ * Kill the buffer ahead of the current cursor position.
+ *
+ * @return true if successful
+ */
+ public boolean killLine() throws IOException {
+ int cp = buf.cursor;
+ int len = buf.buffer.length();
+
+ if (cp >= len) {
+ return false;
+ }
+
+ int num = len - cp;
+ clearAhead(num, 0);
+
+ char[] killed = new char[num];
+ buf.buffer.getChars(cp, (cp + num), killed, 0);
+ buf.buffer.delete(cp, (cp + num));
+
+ String copy = new String(killed);
+ killRing.add(copy);
+
+ return true;
+ }
+
+ public boolean yank() throws IOException {
+ String yanked = killRing.yank();
+
+ if (yanked == null) {
+ return false;
+ }
+ putString(yanked);
+ return true;
+ }
+
+ public boolean yankPop() throws IOException {
+ if (!killRing.lastYank()) {
+ return false;
+ }
+ String current = killRing.yank();
+ if (current == null) {
+ // This shouldn't happen.
+ return false;
+ }
+ backspace(current.length());
+ String yanked = killRing.yankPop();
+ if (yanked == null) {
+ // This shouldn't happen.
+ return false;
+ }
+
+ putString(yanked);
+ return true;
+ }
+
+ /**
+ * Clear the screen by issuing the ANSI "clear screen" code.
+ */
+ public boolean clearScreen() throws IOException {
+ if (!terminal.isAnsiSupported()) {
+ return false;
+ }
+
+ // send the ANSI code to clear the screen
+ printAnsiSequence("2J");
+
+ // then send the ANSI code to go to position 1,1
+ printAnsiSequence("1;1H");
+
+ return true;
+ }
+
+ /**
+ * Issue an audible keyboard bell.
+ */
+ public void beep() throws IOException {
+ if (bellEnabled) {
+ print(KEYBOARD_BELL);
+ // need to flush so the console actually beeps
+ flush();
+ }
+ }
+
+ //disabled to avoid dependency on java.desktop:
+// /**
+// * Paste the contents of the clipboard into the console buffer
+// *
+// * @return true if clipboard contents pasted
+// */
+// public boolean paste() throws IOException {
+// Clipboard clipboard;
+// try { // May throw ugly exception on system without X
+// clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+// }
+// catch (Exception e) {
+// return false;
+// }
+//
+// if (clipboard == null) {
+// return false;
+// }
+//
+// Transferable transferable = clipboard.getContents(null);
+//
+// if (transferable == null) {
+// return false;
+// }
+//
+// try {
+// @SuppressWarnings("deprecation")
+// Object content = transferable.getTransferData(DataFlavor.plainTextFlavor);
+//
+// // This fix was suggested in bug #1060649 at
+// // http://sourceforge.net/tracker/index.php?func=detail&aid=1060649&group_id=64033&atid=506056
+// // to get around the deprecated DataFlavor.plainTextFlavor, but it
+// // raises a UnsupportedFlavorException on Mac OS X
+//
+// if (content == null) {
+// try {
+// content = new DataFlavor().getReaderForText(transferable);
+// }
+// catch (Exception e) {
+// // ignore
+// }
+// }
+//
+// if (content == null) {
+// return false;
+// }
+//
+// String value;
+//
+// if (content instanceof Reader) {
+// // TODO: we might want instead connect to the input stream
+// // so we can interpret individual lines
+// value = "";
+// String line;
+//
+// BufferedReader read = new BufferedReader((Reader) content);
+// while ((line = read.readLine()) != null) {
+// if (value.length() > 0) {
+// value += "\n";
+// }
+//
+// value += line;
+// }
+// }
+// else {
+// value = content.toString();
+// }
+//
+// if (value == null) {
+// return true;
+// }
+//
+// putString(value);
+//
+// return true;
+// }
+// catch (UnsupportedFlavorException e) {
+// Log.error("Paste failed: ", e);
+//
+// return false;
+// }
+// }
+
+ //disabled to avoid dependency on java.desktop:
+// //
+// // Triggered Actions
+// //
+//
+// private final Map<Character, ActionListener> triggeredActions = new HashMap<Character, ActionListener>();
+//
+// /**
+// * Adding a triggered Action allows to give another curse of action if a character passed the pre-processing.
+// * <p/>
+// * Say you want to close the application if the user enter q.
+// * addTriggerAction('q', new ActionListener(){ System.exit(0); }); would do the trick.
+// */
+// public void addTriggeredAction(final char c, final ActionListener listener) {
+// triggeredActions.put(c, listener);
+// }
+
+ //
+ // Formatted Output
+ //
+
+ /**
+ * Output the specified {@link Collection} in proper columns.
+ */
+ public void printColumns(final Collection<? extends CharSequence> items) throws IOException {
+ if (items == null || items.isEmpty()) {
+ return;
+ }
+
+ int width = getTerminal().getWidth();
+ int height = getTerminal().getHeight();
+
+ int maxWidth = 0;
+ for (CharSequence item : items) {
+ maxWidth = Math.max(maxWidth, item.length());
+ }
+ maxWidth = maxWidth + 3;
+ Log.debug("Max width: ", maxWidth);
+
+ int showLines;
+ if (isPaginationEnabled()) {
+ showLines = height - 1; // page limit
+ }
+ else {
+ showLines = Integer.MAX_VALUE;
+ }
+
+ StringBuilder buff = new StringBuilder();
+ for (CharSequence item : items) {
+ if ((buff.length() + maxWidth) > width) {
+ println(buff);
+ buff.setLength(0);
+
+ if (--showLines == 0) {
+ // Overflow
+ print(resources.getString("DISPLAY_MORE"));
+ flush();
+ int c = readCharacter();
+ if (c == '\r' || c == '\n') {
+ // one step forward
+ showLines = 1;
+ }
+ else if (c != 'q') {
+ // page forward
+ showLines = height - 1;
+ }
+
+ back(resources.getString("DISPLAY_MORE").length());
+ if (c == 'q') {
+ // cancel
+ break;
+ }
+ }
+ }
+
+ // NOTE: toString() is important here due to AnsiString being retarded
+ buff.append(item.toString());
+ for (int i = 0; i < (maxWidth - item.length()); i++) {
+ buff.append(' ');
+ }
+ }
+
+ if (buff.length() > 0) {
+ println(buff);
+ }
+ }
+
+ //
+ // Non-supported Terminal Support
+ //
+
+ private Thread maskThread;
+
+ private void beforeReadLine(final String prompt, final Character mask) {
+ if (mask != null && maskThread == null) {
+ final String fullPrompt = "\r" + prompt
+ + " "
+ + " "
+ + " "
+ + "\r" + prompt;
+
+ maskThread = new Thread()
+ {
+ public void run() {
+ while (!interrupted()) {
+ try {
+ Writer out = getOutput();
+ out.write(fullPrompt);
+ out.flush();
+ sleep(3);
+ }
+ catch (IOException e) {
+ return;
+ }
+ catch (InterruptedException e) {
+ return;
+ }
+ }
+ }
+ };
+
+ maskThread.setPriority(Thread.MAX_PRIORITY);
+ maskThread.setDaemon(true);
+ maskThread.start();
+ }
+ }
+
+ private void afterReadLine() {
+ if (maskThread != null && maskThread.isAlive()) {
+ maskThread.interrupt();
+ }
+
+ maskThread = null;
+ }
+
+ /**
+ * Erases the current line with the existing prompt, then redraws the line
+ * with the provided prompt and buffer
+ * @param prompt
+ * the new prompt
+ * @param buffer
+ * the buffer to be drawn
+ * @param cursorDest
+ * where you want the cursor set when the line has been drawn.
+ * -1 for end of line.
+ * */
+ public void resetPromptLine(String prompt, String buffer, int cursorDest) throws IOException {
+ // move cursor to end of line
+ moveToEnd();
+
+ // backspace all text, including prompt
+ buf.buffer.append(this.prompt);
+ int promptLength = 0;
+ if (this.prompt != null) {
+ promptLength = this.prompt.length();
+ }
+
+ buf.cursor += promptLength;
+ setPrompt("");
+ backspaceAll();
+
+ setPrompt(prompt);
+ redrawLine();
+ setBuffer(buffer);
+
+ // move cursor to destination (-1 will move to end of line)
+ if (cursorDest < 0) cursorDest = buffer.length();
+ setCursorPosition(cursorDest);
+
+ flush();
+ }
+
+ public void printSearchStatus(String searchTerm, String match) throws IOException {
+ printSearchStatus(searchTerm, match, "(reverse-i-search)`");
+ }
+
+ public void printForwardSearchStatus(String searchTerm, String match) throws IOException {
+ printSearchStatus(searchTerm, match, "(i-search)`");
+ }
+
+ private void printSearchStatus(String searchTerm, String match, String searchLabel) throws IOException {
+ String prompt = searchLabel + searchTerm + "': ";
+ int cursorDest = match.indexOf(searchTerm);
+ resetPromptLine(prompt, match, cursorDest);
+ }
+
+ public void restoreLine(String originalPrompt, int cursorDest) throws IOException {
+ // TODO move cursor to matched string
+ String prompt = lastLine(originalPrompt);
+ String buffer = buf.buffer.toString();
+ resetPromptLine(prompt, buffer, cursorDest);
+ }
+
+ //
+ // History search
+ //
+ /**
+ * Search backward in history from a given position.
+ *
+ * @param searchTerm substring to search for.
+ * @param startIndex the index from which on to search
+ * @return index where this substring has been found, or -1 else.
+ */
+ public int searchBackwards(String searchTerm, int startIndex) {
+ return searchBackwards(searchTerm, startIndex, false);
+ }
+
+ /**
+ * Search backwards in history from the current position.
+ *
+ * @param searchTerm substring to search for.
+ * @return index where the substring has been found, or -1 else.
+ */
+ public int searchBackwards(String searchTerm) {
+ return searchBackwards(searchTerm, history.index());
+ }
+
+
+ public int searchBackwards(String searchTerm, int startIndex, boolean startsWith) {
+ ListIterator<History.Entry> it = history.entries(startIndex);
+ while (it.hasPrevious()) {
+ History.Entry e = it.previous();
+ if (startsWith) {
+ if (e.value().toString().startsWith(searchTerm)) {
+ return e.index();
+ }
+ } else {
+ if (e.value().toString().contains(searchTerm)) {
+ return e.index();
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Search forward in history from a given position.
+ *
+ * @param searchTerm substring to search for.
+ * @param startIndex the index from which on to search
+ * @return index where this substring has been found, or -1 else.
+ */
+ public int searchForwards(String searchTerm, int startIndex) {
+ return searchForwards(searchTerm, startIndex, false);
+ }
+ /**
+ * Search forwards in history from the current position.
+ *
+ * @param searchTerm substring to search for.
+ * @return index where the substring has been found, or -1 else.
+ */
+ public int searchForwards(String searchTerm) {
+ return searchForwards(searchTerm, history.index());
+ }
+
+ public int searchForwards(String searchTerm, int startIndex, boolean startsWith) {
+ if (startIndex >= history.size()) {
+ startIndex = history.size() - 1;
+ }
+
+ ListIterator<History.Entry> it = history.entries(startIndex);
+
+ if (searchIndex != -1 && it.hasNext()) {
+ it.next();
+ }
+
+ while (it.hasNext()) {
+ History.Entry e = it.next();
+ if (startsWith) {
+ if (e.value().toString().startsWith(searchTerm)) {
+ return e.index();
+ }
+ } else {
+ if (e.value().toString().contains(searchTerm)) {
+ return e.index();
+ }
+ }
+ }
+ return -1;
+ }
+
+ //
+ // Helpers
+ //
+
+ /**
+ * Checks to see if the specified character is a delimiter. We consider a
+ * character a delimiter if it is anything but a letter or digit.
+ *
+ * @param c The character to test
+ * @return True if it is a delimiter
+ */
+ private boolean isDelimiter(final char c) {
+ return !Character.isLetterOrDigit(c);
+ }
+
+ /**
+ * Checks to see if a character is a whitespace character. Currently
+ * this delegates to {@link Character#isWhitespace(char)}, however
+ * eventually it should be hooked up so that the definition of whitespace
+ * can be configured, as readline does.
+ *
+ * @param c The character to check
+ * @return true if the character is a whitespace
+ */
+ private boolean isWhitespace(final char c) {
+ return Character.isWhitespace (c);
+ }
+
+ private void printAnsiSequence(String sequence) throws IOException {
+ print(27);
+ print('[');
+ print(sequence);
+ flush(); // helps with step debugging
+ }
+
+}