src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/LineReaderImpl.java
changeset 58903 eeb1c0da2126
parent 52938 5ff7480c9e28
--- a/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/LineReaderImpl.java	Mon Nov 04 14:26:18 2019 +0800
+++ b/src/jdk.internal.le/share/classes/jdk/internal/org/jline/reader/impl/LineReaderImpl.java	Mon Nov 04 09:40:35 2019 +0100
@@ -1,10 +1,10 @@
 /*
- * Copyright (c) 2002-2018, the original author or authors.
+ * Copyright (c) 2002-2019, 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
+ * https://opensource.org/licenses/BSD-3-Clause
  */
 package jdk.internal.org.jline.reader.impl;
 
@@ -17,10 +17,13 @@
 import java.util.*;
 import java.util.Map.Entry;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.*;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
 import jdk.internal.org.jline.keymap.BindingReader;
@@ -32,6 +35,7 @@
 import jdk.internal.org.jline.terminal.Attributes.ControlChar;
 import jdk.internal.org.jline.terminal.Terminal.Signal;
 import jdk.internal.org.jline.terminal.Terminal.SignalHandler;
+import jdk.internal.org.jline.terminal.impl.AbstractWindowsTerminal;
 import jdk.internal.org.jline.utils.AttributedString;
 import jdk.internal.org.jline.utils.AttributedStringBuilder;
 import jdk.internal.org.jline.utils.AttributedStyle;
@@ -159,8 +163,8 @@
 
     protected final Size size = new Size();
 
-    protected AttributedString prompt;
-    protected AttributedString rightPrompt;
+    protected AttributedString prompt = AttributedString.EMPTY;
+    protected AttributedString rightPrompt = AttributedString.EMPTY;
 
     protected MaskingCallback maskingCallback;
 
@@ -210,6 +214,10 @@
     protected UndoTree<Buffer> undo = new UndoTree<>(this::setBuffer);
     protected boolean isUndo;
 
+    /**
+     * State lock
+     */
+    protected final ReentrantLock lock = new ReentrantLock();
     /*
      * Current internal state of the line reader
      */
@@ -239,6 +247,11 @@
 
     protected int smallTerminalOffset = 0;
 
+    /*
+     * accept-and-infer-next-history, accept-and-hold & accept-line-and-down-history
+     */
+    protected boolean nextCommandFromHistory = false;
+    protected int nextHistoryId = -1;
 
 
     public LineReaderImpl(Terminal terminal) throws IOException {
@@ -266,6 +279,7 @@
         builtinWidgets = builtinWidgets();
         widgets = new HashMap<>(builtinWidgets);
         bindingReader = new BindingReader(terminal.reader());
+        doDisplay();
     }
 
     public Terminal getTerminal() {
@@ -467,8 +481,7 @@
         SignalHandler previousWinchHandler = null;
         SignalHandler previousContHandler = null;
         Attributes originalAttributes = null;
-        boolean dumb = Terminal.TYPE_DUMB.equals(terminal.getType())
-                    || Terminal.TYPE_DUMB_COLOR.equals(terminal.getType());
+        boolean dumb = isTerminalDumb();
         try {
 
             this.maskingCallback = maskingCallback;
@@ -495,6 +508,17 @@
             if (buffer != null) {
                 buf.write(buffer);
             }
+            if (nextCommandFromHistory && nextHistoryId > 0) {
+                if (history.size() > nextHistoryId) {
+                    history.moveTo(nextHistoryId);
+                } else {
+                    history.moveTo(history.last());
+                }
+                buf.write(history.current());
+            } else {
+                nextHistoryId = -1;
+            }
+            nextCommandFromHistory = false;
             undo.clear();
             parsedLine = null;
             keyMap = MAIN;
@@ -503,7 +527,9 @@
                 history.attach(this);
             }
 
-            synchronized (this) {
+            try {
+                lock.lock();
+
                 this.reading = true;
 
                 previousIntrHandler = terminal.handle(Signal.INT, signal -> readLineThread.interrupt());
@@ -511,18 +537,7 @@
                 previousContHandler = terminal.handle(Signal.CONT, this::handleSignal);
                 originalAttributes = terminal.enterRawMode();
 
-                // Cache terminal size for the duration of the call to readLine()
-                // It will eventually be updated with WINCH signals
-                size.copy(terminal.getSize());
-
-                display = new Display(terminal, false);
-                if (size.getRows() == 0 || size.getColumns() == 0) {
-                    display.resize(1, Integer.MAX_VALUE);
-                } else {
-                    display.resize(size.getRows(), size.getColumns());
-                }
-                if (isSet(Option.DELAY_LINE_WRAP))
-                    display.setDelayLineWrap(true);
+                doDisplay();
 
                 // Move into application mode
                 if (!dumb) {
@@ -547,6 +562,8 @@
                 // Draw initial prompt
                 redrawLine();
                 redisplay();
+            } finally {
+                lock.unlock();
             }
 
             while (true) {
@@ -578,7 +595,8 @@
                     regionActive = RegionType.NONE;
                 }
 
-                synchronized (this) {
+                try {
+                    lock.lock();
                     // Get executable widget
                     Buffer copy = buf.copy();
                     Widget w = getWidget(o);
@@ -610,6 +628,8 @@
                     if (!dumb) {
                         redisplay();
                     }
+                } finally {
+                    lock.unlock();
                 }
             }
         } catch (IOError e) {
@@ -620,7 +640,9 @@
             }
         }
         finally {
-            synchronized (this) {
+            try {
+                lock.lock();
+
                 this.reading = false;
 
                 cleanup();
@@ -636,26 +658,54 @@
                 if (previousContHandler != null) {
                     terminal.handle(Signal.CONT, previousContHandler);
                 }
+            } finally {
+                lock.unlock();
             }
             startedReading.set(false);
         }
     }
 
+    private boolean isTerminalDumb(){
+        return Terminal.TYPE_DUMB.equals(terminal.getType())
+                || Terminal.TYPE_DUMB_COLOR.equals(terminal.getType());
+    }
+
+    private void doDisplay(){
+        // Cache terminal size for the duration of the call to readLine()
+        // It will eventually be updated with WINCH signals
+        size.copy(terminal.getBufferSize());
+
+        display = new Display(terminal, false);
+        if (size.getRows() == 0 || size.getColumns() == 0) {
+            display.resize(1, Integer.MAX_VALUE);
+        } else {
+            display.resize(size.getRows(), size.getColumns());
+        }
+        if (isSet(Option.DELAY_LINE_WRAP))
+            display.setDelayLineWrap(true);
+    }
+
     @Override
-    public synchronized void printAbove(String str) {
-        boolean reading = this.reading;
-        if (reading) {
-            display.update(Collections.emptyList(), 0);
-        }
-        if (str.endsWith("\n")) {
-            terminal.writer().print(str);
-        } else {
-            terminal.writer().println(str);
-        }
-        if (reading) {
-            redisplay(false);
-        }
-        terminal.flush();
+    public void printAbove(String str) {
+        try {
+            lock.lock();
+
+            boolean reading = this.reading;
+            if (reading) {
+                display.update(Collections.emptyList(), 0);
+            }
+            if (str.endsWith("\n") || str.endsWith("\n\033[m") || str.endsWith("\n\033[0m")) {
+                terminal.writer().print(str);
+            } else {
+                terminal.writer().println(str);
+            }
+            if (reading) {
+                redisplay(false);
+            }
+            terminal.flush();
+        } finally {
+            lock.unlock();
+        }
     }
 
     @Override
@@ -664,8 +714,13 @@
     }
 
     @Override
-    public synchronized boolean isReading() {
-        return reading;
+    public boolean isReading() {
+        try {
+            lock.lock();
+            return reading;
+        } finally {
+            lock.unlock();
+        }
     }
 
     /* Make sure we position the cursor on column 0 */
@@ -700,27 +755,32 @@
             sb.append(" ");
             sb.append(KeyMap.key(terminal, Capability.carriage_return));
         }
-        print(sb.toAnsi(terminal));
+        sb.print(terminal);
         return true;
     }
 
     @Override
-    public synchronized void callWidget(String name) {
-        if (!reading) {
-            throw new IllegalStateException("Widgets can only be called during a `readLine` call");
-        }
+    public void callWidget(String name) {
         try {
-            Widget w;
-            if (name.startsWith(".")) {
-                w = builtinWidgets.get(name.substring(1));
-            } else {
-                w = widgets.get(name);
-            }
-            if (w != null) {
-                w.apply();
-            }
-        } catch (Throwable t) {
-            Log.debug("Error executing widget '", name, "'", t);
+            lock.lock();
+            if (!reading) {
+                throw new IllegalStateException("Widgets can only be called during a `readLine` call");
+            }
+            try {
+                Widget w;
+                if (name.startsWith(".")) {
+                    w = builtinWidgets.get(name.substring(1));
+                } else {
+                    w = widgets.get(name);
+                }
+                if (w != null) {
+                    w.apply();
+                }
+            } catch (Throwable t) {
+                Log.debug("Error executing widget '", name, "'", t);
+            }
+        } finally {
+            lock.unlock();
         }
     }
 
@@ -760,13 +820,35 @@
      * @return the character, or -1 if an EOF is received.
      */
     public int readCharacter() {
-        return bindingReader.readCharacter();
+        if (lock.isHeldByCurrentThread()) {
+            try {
+                lock.unlock();
+                return bindingReader.readCharacter();
+            } finally {
+                lock.lock();
+            }
+        } else {
+            return bindingReader.readCharacter();
+        }
     }
 
     public int peekCharacter(long timeout) {
         return bindingReader.peekCharacter(timeout);
     }
 
+    protected <T> T doReadBinding(KeyMap<T> keys, KeyMap<T> local) {
+        if (lock.isHeldByCurrentThread()) {
+            try {
+                lock.unlock();
+                return bindingReader.readBinding(keys, local);
+            } finally {
+                lock.lock();
+            }
+        } else {
+            return bindingReader.readBinding(keys, local);
+        }
+    }
+
     /**
      * Read from the input stream and decode an operation from the key map.
      *
@@ -783,7 +865,7 @@
     }
 
     public Binding readBinding(KeyMap<Binding> keys, KeyMap<Binding> local) {
-        Binding o = bindingReader.readBinding(keys, local);
+        Binding o = doReadBinding(keys, local);
         /*
          * The kill ring keeps record of whether or not the
          * previous command was a yank or a kill. We reset
@@ -926,7 +1008,7 @@
                     if (ch != '\n') {
                         sb.append(ch);
                     }
-                } else if (ch == '\\') {
+                } else if (parser.isEscapeChar(ch)) {
                     escaped = true;
                 } else {
                     sb.append(ch);
@@ -948,13 +1030,18 @@
 
     protected void handleSignal(Signal signal) {
         if (signal == Signal.WINCH) {
-            size.copy(terminal.getSize());
+            Status status = Status.getStatus(terminal, false);
+            if (status != null) {
+                status.hardReset();
+            }
+            size.copy(terminal.getBufferSize());
             display.resize(size.getRows(), size.getColumns());
+            redrawLine();
             redisplay();
         }
         else if (signal == Signal.CONT) {
             terminal.enterRawMode();
-            size.copy(terminal.getSize());
+            size.copy(terminal.getBufferSize());
             display.resize(size.getRows(), size.getColumns());
             terminal.puts(Capability.keypad_xmit);
             redrawLine();
@@ -1903,7 +1990,7 @@
         while (true) {
             post = () -> new AttributedString(searchPrompt + searchBuffer.toString() + "_");
             redisplay();
-            Binding b = bindingReader.readBinding(keyMap);
+            Binding b = doReadBinding(keyMap, null);
             if (b instanceof Reference) {
                 String func = ((Reference) b).name();
                 switch (func) {
@@ -2300,7 +2387,7 @@
         } else {
             viMoveMode = mode;
             mark = -1;
-            Binding b = bindingReader.readBinding(getKeys(), keyMaps.get(VIOPP));
+            Binding b = doReadBinding(getKeys(), keyMaps.get(VIOPP));
             if (b == null || new Reference(SEND_BREAK).equals(b)) {
                 viMoveMode = ViMoveMode.NORMAL;
                 mark = oldMark;
@@ -2710,6 +2797,42 @@
         return acceptLine();
     }
 
+    protected boolean acceptAndHold() {
+        nextCommandFromHistory = false;
+        acceptLine();
+        if (!buf.toString().isEmpty()) {
+            nextHistoryId = Integer.MAX_VALUE;
+            nextCommandFromHistory = true;
+        }
+        return nextCommandFromHistory;
+    }
+
+    protected boolean acceptLineAndDownHistory() {
+        nextCommandFromHistory = false;
+        acceptLine();
+        if (nextHistoryId < 0) {
+            nextHistoryId = history.index();
+        }
+        if (history.size() > nextHistoryId + 1) {
+            nextHistoryId++;
+            nextCommandFromHistory = true;
+        }
+        return nextCommandFromHistory;
+    }
+
+    protected boolean acceptAndInferNextHistory() {
+        nextCommandFromHistory = false;
+        acceptLine();
+        if (!buf.toString().isEmpty()) {
+            nextHistoryId = searchBackwards(buf.toString(), history.last());
+            if (nextHistoryId >= 0 && history.size() > nextHistoryId + 1) {
+                nextHistoryId++;
+                nextCommandFromHistory = true;
+            }
+        }
+        return nextCommandFromHistory;
+    }
+
     protected boolean acceptLine() {
         parsedLine = null;
         if (!isSet(Option.DISABLE_EVENT_EXPANSION)) {
@@ -3343,255 +3466,317 @@
 
     protected Map<String, Widget> builtinWidgets() {
         Map<String, Widget> widgets = new HashMap<>();
-        widgets.put(ACCEPT_LINE, this::acceptLine);
-        widgets.put(ARGUMENT_BASE, this::argumentBase);
-        widgets.put(BACKWARD_CHAR, this::backwardChar);
-        widgets.put(BACKWARD_DELETE_CHAR, this::backwardDeleteChar);
-        widgets.put(BACKWARD_DELETE_WORD, this::backwardDeleteWord);
-        widgets.put(BACKWARD_KILL_LINE, this::backwardKillLine);
-        widgets.put(BACKWARD_KILL_WORD, this::backwardKillWord);
-        widgets.put(BACKWARD_WORD, this::backwardWord);
-        widgets.put(BEEP, this::beep);
-        widgets.put(BEGINNING_OF_BUFFER_OR_HISTORY, this::beginningOfBufferOrHistory);
-        widgets.put(BEGINNING_OF_HISTORY, this::beginningOfHistory);
-        widgets.put(BEGINNING_OF_LINE, this::beginningOfLine);
-        widgets.put(BEGINNING_OF_LINE_HIST, this::beginningOfLineHist);
-        widgets.put(CAPITALIZE_WORD, this::capitalizeWord);
-        widgets.put(CLEAR, this::clear);
-        widgets.put(CLEAR_SCREEN, this::clearScreen);
-        widgets.put(COMPLETE_PREFIX, this::completePrefix);
-        widgets.put(COMPLETE_WORD, this::completeWord);
-        widgets.put(COPY_PREV_WORD, this::copyPrevWord);
-        widgets.put(COPY_REGION_AS_KILL, this::copyRegionAsKill);
-        widgets.put(DELETE_CHAR, this::deleteChar);
-        widgets.put(DELETE_CHAR_OR_LIST, this::deleteCharOrList);
-        widgets.put(DELETE_WORD, this::deleteWord);
-        widgets.put(DIGIT_ARGUMENT, this::digitArgument);
-        widgets.put(DO_LOWERCASE_VERSION, this::doLowercaseVersion);
-        widgets.put(DOWN_CASE_WORD, this::downCaseWord);
-        widgets.put(DOWN_LINE, this::downLine);
-        widgets.put(DOWN_LINE_OR_HISTORY, this::downLineOrHistory);
-        widgets.put(DOWN_LINE_OR_SEARCH, this::downLineOrSearch);
-        widgets.put(DOWN_HISTORY, this::downHistory);
-        widgets.put(EMACS_EDITING_MODE, this::emacsEditingMode);
-        widgets.put(EMACS_BACKWARD_WORD, this::emacsBackwardWord);
-        widgets.put(EMACS_FORWARD_WORD, this::emacsForwardWord);
-        widgets.put(END_OF_BUFFER_OR_HISTORY, this::endOfBufferOrHistory);
-        widgets.put(END_OF_HISTORY, this::endOfHistory);
-        widgets.put(END_OF_LINE, this::endOfLine);
-        widgets.put(END_OF_LINE_HIST, this::endOfLineHist);
-        widgets.put(EXCHANGE_POINT_AND_MARK, this::exchangePointAndMark);
-        widgets.put(EXPAND_HISTORY, this::expandHistory);
-        widgets.put(EXPAND_OR_COMPLETE, this::expandOrComplete);
-        widgets.put(EXPAND_OR_COMPLETE_PREFIX, this::expandOrCompletePrefix);
-        widgets.put(EXPAND_WORD, this::expandWord);
-        widgets.put(FRESH_LINE, this::freshLine);
-        widgets.put(FORWARD_CHAR, this::forwardChar);
-        widgets.put(FORWARD_WORD, this::forwardWord);
-        widgets.put(HISTORY_INCREMENTAL_SEARCH_BACKWARD, this::historyIncrementalSearchBackward);
-        widgets.put(HISTORY_INCREMENTAL_SEARCH_FORWARD, this::historyIncrementalSearchForward);
-        widgets.put(HISTORY_SEARCH_BACKWARD, this::historySearchBackward);
-        widgets.put(HISTORY_SEARCH_FORWARD, this::historySearchForward);
-        widgets.put(INSERT_CLOSE_CURLY, this::insertCloseCurly);
-        widgets.put(INSERT_CLOSE_PAREN, this::insertCloseParen);
-        widgets.put(INSERT_CLOSE_SQUARE, this::insertCloseSquare);
-        widgets.put(INSERT_COMMENT, this::insertComment);
-        widgets.put(KILL_BUFFER, this::killBuffer);
-        widgets.put(KILL_LINE, this::killLine);
-        widgets.put(KILL_REGION, this::killRegion);
-        widgets.put(KILL_WHOLE_LINE, this::killWholeLine);
-        widgets.put(KILL_WORD, this::killWord);
-        widgets.put(LIST_CHOICES, this::listChoices);
-        widgets.put(MENU_COMPLETE, this::menuComplete);
-        widgets.put(MENU_EXPAND_OR_COMPLETE, this::menuExpandOrComplete);
-        widgets.put(NEG_ARGUMENT, this::negArgument);
-        widgets.put(OVERWRITE_MODE, this::overwriteMode);
-//        widgets.put(QUIT, this::quit);
-        widgets.put(QUOTED_INSERT, this::quotedInsert);
-        widgets.put(REDISPLAY, this::redisplay);
-        widgets.put(REDRAW_LINE, this::redrawLine);
-        widgets.put(REDO, this::redo);
-        widgets.put(SELF_INSERT, this::selfInsert);
-        widgets.put(SELF_INSERT_UNMETA, this::selfInsertUnmeta);
-        widgets.put(SEND_BREAK, this::sendBreak);
-        widgets.put(SET_MARK_COMMAND, this::setMarkCommand);
-        widgets.put(TRANSPOSE_CHARS, this::transposeChars);
-        widgets.put(TRANSPOSE_WORDS, this::transposeWords);
-        widgets.put(UNDEFINED_KEY, this::undefinedKey);
-        widgets.put(UNIVERSAL_ARGUMENT, this::universalArgument);
-        widgets.put(UNDO, this::undo);
-        widgets.put(UP_CASE_WORD, this::upCaseWord);
-        widgets.put(UP_HISTORY, this::upHistory);
-        widgets.put(UP_LINE, this::upLine);
-        widgets.put(UP_LINE_OR_HISTORY, this::upLineOrHistory);
-        widgets.put(UP_LINE_OR_SEARCH, this::upLineOrSearch);
-        widgets.put(VI_ADD_EOL, this::viAddEol);
-        widgets.put(VI_ADD_NEXT, this::viAddNext);
-        widgets.put(VI_BACKWARD_CHAR, this::viBackwardChar);
-        widgets.put(VI_BACKWARD_DELETE_CHAR, this::viBackwardDeleteChar);
-        widgets.put(VI_BACKWARD_BLANK_WORD, this::viBackwardBlankWord);
-        widgets.put(VI_BACKWARD_BLANK_WORD_END, this::viBackwardBlankWordEnd);
-        widgets.put(VI_BACKWARD_KILL_WORD, this::viBackwardKillWord);
-        widgets.put(VI_BACKWARD_WORD, this::viBackwardWord);
-        widgets.put(VI_BACKWARD_WORD_END, this::viBackwardWordEnd);
-        widgets.put(VI_BEGINNING_OF_LINE, this::viBeginningOfLine);
-        widgets.put(VI_CMD_MODE, this::viCmdMode);
-        widgets.put(VI_DIGIT_OR_BEGINNING_OF_LINE, this::viDigitOrBeginningOfLine);
-        widgets.put(VI_DOWN_LINE_OR_HISTORY, this::viDownLineOrHistory);
-        widgets.put(VI_CHANGE, this::viChange);
-        widgets.put(VI_CHANGE_EOL, this::viChangeEol);
-        widgets.put(VI_CHANGE_WHOLE_LINE, this::viChangeWholeLine);
-        widgets.put(VI_DELETE_CHAR, this::viDeleteChar);
-        widgets.put(VI_DELETE, this::viDelete);
-        widgets.put(VI_END_OF_LINE, this::viEndOfLine);
-        widgets.put(VI_KILL_EOL, this::viKillEol);
-        widgets.put(VI_FIRST_NON_BLANK, this::viFirstNonBlank);
-        widgets.put(VI_FIND_NEXT_CHAR, this::viFindNextChar);
-        widgets.put(VI_FIND_NEXT_CHAR_SKIP, this::viFindNextCharSkip);
-        widgets.put(VI_FIND_PREV_CHAR, this::viFindPrevChar);
-        widgets.put(VI_FIND_PREV_CHAR_SKIP, this::viFindPrevCharSkip);
-        widgets.put(VI_FORWARD_BLANK_WORD, this::viForwardBlankWord);
-        widgets.put(VI_FORWARD_BLANK_WORD_END, this::viForwardBlankWordEnd);
-        widgets.put(VI_FORWARD_CHAR, this::viForwardChar);
-        widgets.put(VI_FORWARD_WORD, this::viForwardWord);
-        widgets.put(VI_FORWARD_WORD, this::viForwardWord);
-        widgets.put(VI_FORWARD_WORD_END, this::viForwardWordEnd);
-        widgets.put(VI_HISTORY_SEARCH_BACKWARD, this::viHistorySearchBackward);
-        widgets.put(VI_HISTORY_SEARCH_FORWARD, this::viHistorySearchForward);
-        widgets.put(VI_INSERT, this::viInsert);
-        widgets.put(VI_INSERT_BOL, this::viInsertBol);
-        widgets.put(VI_INSERT_COMMENT, this::viInsertComment);
-        widgets.put(VI_JOIN, this::viJoin);
-        widgets.put(VI_KILL_LINE, this::viKillWholeLine);
-        widgets.put(VI_MATCH_BRACKET, this::viMatchBracket);
-        widgets.put(VI_OPEN_LINE_ABOVE, this::viOpenLineAbove);
-        widgets.put(VI_OPEN_LINE_BELOW, this::viOpenLineBelow);
-        widgets.put(VI_PUT_AFTER, this::viPutAfter);
-        widgets.put(VI_PUT_BEFORE, this::viPutBefore);
-        widgets.put(VI_REPEAT_FIND, this::viRepeatFind);
-        widgets.put(VI_REPEAT_SEARCH, this::viRepeatSearch);
-        widgets.put(VI_REPLACE_CHARS, this::viReplaceChars);
-        widgets.put(VI_REV_REPEAT_FIND, this::viRevRepeatFind);
-        widgets.put(VI_REV_REPEAT_SEARCH, this::viRevRepeatSearch);
-        widgets.put(VI_SWAP_CASE, this::viSwapCase);
-        widgets.put(VI_UP_LINE_OR_HISTORY, this::viUpLineOrHistory);
-        widgets.put(VI_YANK, this::viYankTo);
-        widgets.put(VI_YANK_WHOLE_LINE, this::viYankWholeLine);
-        widgets.put(VISUAL_LINE_MODE, this::visualLineMode);
-        widgets.put(VISUAL_MODE, this::visualMode);
-        widgets.put(WHAT_CURSOR_POSITION, this::whatCursorPosition);
-        widgets.put(YANK, this::yank);
-        widgets.put(YANK_POP, this::yankPop);
-        widgets.put(MOUSE, this::mouse);
-        widgets.put(BEGIN_PASTE, this::beginPaste);
-        widgets.put(FOCUS_IN, this::focusIn);
-        widgets.put(FOCUS_OUT, this::focusOut);
+        addBuiltinWidget(widgets, ACCEPT_AND_INFER_NEXT_HISTORY, this::acceptAndInferNextHistory);
+        addBuiltinWidget(widgets, ACCEPT_AND_HOLD, this::acceptAndHold);
+        addBuiltinWidget(widgets, ACCEPT_LINE, this::acceptLine);
+        addBuiltinWidget(widgets, ACCEPT_LINE_AND_DOWN_HISTORY, this::acceptLineAndDownHistory);
+        addBuiltinWidget(widgets, ARGUMENT_BASE, this::argumentBase);
+        addBuiltinWidget(widgets, BACKWARD_CHAR, this::backwardChar);
+        addBuiltinWidget(widgets, BACKWARD_DELETE_CHAR, this::backwardDeleteChar);
+        addBuiltinWidget(widgets, BACKWARD_DELETE_WORD, this::backwardDeleteWord);
+        addBuiltinWidget(widgets, BACKWARD_KILL_LINE, this::backwardKillLine);
+        addBuiltinWidget(widgets, BACKWARD_KILL_WORD, this::backwardKillWord);
+        addBuiltinWidget(widgets, BACKWARD_WORD, this::backwardWord);
+        addBuiltinWidget(widgets, BEEP, this::beep);
+        addBuiltinWidget(widgets, BEGINNING_OF_BUFFER_OR_HISTORY, this::beginningOfBufferOrHistory);
+        addBuiltinWidget(widgets, BEGINNING_OF_HISTORY, this::beginningOfHistory);
+        addBuiltinWidget(widgets, BEGINNING_OF_LINE, this::beginningOfLine);
+        addBuiltinWidget(widgets, BEGINNING_OF_LINE_HIST, this::beginningOfLineHist);
+        addBuiltinWidget(widgets, CAPITALIZE_WORD, this::capitalizeWord);
+        addBuiltinWidget(widgets, CLEAR, this::clear);
+        addBuiltinWidget(widgets, CLEAR_SCREEN, this::clearScreen);
+        addBuiltinWidget(widgets, COMPLETE_PREFIX, this::completePrefix);
+        addBuiltinWidget(widgets, COMPLETE_WORD, this::completeWord);
+        addBuiltinWidget(widgets, COPY_PREV_WORD, this::copyPrevWord);
+        addBuiltinWidget(widgets, COPY_REGION_AS_KILL, this::copyRegionAsKill);
+        addBuiltinWidget(widgets, DELETE_CHAR, this::deleteChar);
+        addBuiltinWidget(widgets, DELETE_CHAR_OR_LIST, this::deleteCharOrList);
+        addBuiltinWidget(widgets, DELETE_WORD, this::deleteWord);
+        addBuiltinWidget(widgets, DIGIT_ARGUMENT, this::digitArgument);
+        addBuiltinWidget(widgets, DO_LOWERCASE_VERSION, this::doLowercaseVersion);
+        addBuiltinWidget(widgets, DOWN_CASE_WORD, this::downCaseWord);
+        addBuiltinWidget(widgets, DOWN_LINE, this::downLine);
+        addBuiltinWidget(widgets, DOWN_LINE_OR_HISTORY, this::downLineOrHistory);
+        addBuiltinWidget(widgets, DOWN_LINE_OR_SEARCH, this::downLineOrSearch);
+        addBuiltinWidget(widgets, DOWN_HISTORY, this::downHistory);
+        addBuiltinWidget(widgets, EMACS_EDITING_MODE, this::emacsEditingMode);
+        addBuiltinWidget(widgets, EMACS_BACKWARD_WORD, this::emacsBackwardWord);
+        addBuiltinWidget(widgets, EMACS_FORWARD_WORD, this::emacsForwardWord);
+        addBuiltinWidget(widgets, END_OF_BUFFER_OR_HISTORY, this::endOfBufferOrHistory);
+        addBuiltinWidget(widgets, END_OF_HISTORY, this::endOfHistory);
+        addBuiltinWidget(widgets, END_OF_LINE, this::endOfLine);
+        addBuiltinWidget(widgets, END_OF_LINE_HIST, this::endOfLineHist);
+        addBuiltinWidget(widgets, EXCHANGE_POINT_AND_MARK, this::exchangePointAndMark);
+        addBuiltinWidget(widgets, EXPAND_HISTORY, this::expandHistory);
+        addBuiltinWidget(widgets, EXPAND_OR_COMPLETE, this::expandOrComplete);
+        addBuiltinWidget(widgets, EXPAND_OR_COMPLETE_PREFIX, this::expandOrCompletePrefix);
+        addBuiltinWidget(widgets, EXPAND_WORD, this::expandWord);
+        addBuiltinWidget(widgets, FRESH_LINE, this::freshLine);
+        addBuiltinWidget(widgets, FORWARD_CHAR, this::forwardChar);
+        addBuiltinWidget(widgets, FORWARD_WORD, this::forwardWord);
+        addBuiltinWidget(widgets, HISTORY_INCREMENTAL_SEARCH_BACKWARD, this::historyIncrementalSearchBackward);
+        addBuiltinWidget(widgets, HISTORY_INCREMENTAL_SEARCH_FORWARD, this::historyIncrementalSearchForward);
+        addBuiltinWidget(widgets, HISTORY_SEARCH_BACKWARD, this::historySearchBackward);
+        addBuiltinWidget(widgets, HISTORY_SEARCH_FORWARD, this::historySearchForward);
+        addBuiltinWidget(widgets, INSERT_CLOSE_CURLY, this::insertCloseCurly);
+        addBuiltinWidget(widgets, INSERT_CLOSE_PAREN, this::insertCloseParen);
+        addBuiltinWidget(widgets, INSERT_CLOSE_SQUARE, this::insertCloseSquare);
+        addBuiltinWidget(widgets, INSERT_COMMENT, this::insertComment);
+        addBuiltinWidget(widgets, KILL_BUFFER, this::killBuffer);
+        addBuiltinWidget(widgets, KILL_LINE, this::killLine);
+        addBuiltinWidget(widgets, KILL_REGION, this::killRegion);
+        addBuiltinWidget(widgets, KILL_WHOLE_LINE, this::killWholeLine);
+        addBuiltinWidget(widgets, KILL_WORD, this::killWord);
+        addBuiltinWidget(widgets, LIST_CHOICES, this::listChoices);
+        addBuiltinWidget(widgets, MENU_COMPLETE, this::menuComplete);
+        addBuiltinWidget(widgets, MENU_EXPAND_OR_COMPLETE, this::menuExpandOrComplete);
+        addBuiltinWidget(widgets, NEG_ARGUMENT, this::negArgument);
+        addBuiltinWidget(widgets, OVERWRITE_MODE, this::overwriteMode);
+//        addBuiltinWidget(widgets, QUIT, this::quit);
+        addBuiltinWidget(widgets, QUOTED_INSERT, this::quotedInsert);
+        addBuiltinWidget(widgets, REDISPLAY, this::redisplay);
+        addBuiltinWidget(widgets, REDRAW_LINE, this::redrawLine);
+        addBuiltinWidget(widgets, REDO, this::redo);
+        addBuiltinWidget(widgets, SELF_INSERT, this::selfInsert);
+        addBuiltinWidget(widgets, SELF_INSERT_UNMETA, this::selfInsertUnmeta);
+        addBuiltinWidget(widgets, SEND_BREAK, this::sendBreak);
+        addBuiltinWidget(widgets, SET_MARK_COMMAND, this::setMarkCommand);
+        addBuiltinWidget(widgets, TRANSPOSE_CHARS, this::transposeChars);
+        addBuiltinWidget(widgets, TRANSPOSE_WORDS, this::transposeWords);
+        addBuiltinWidget(widgets, UNDEFINED_KEY, this::undefinedKey);
+        addBuiltinWidget(widgets, UNIVERSAL_ARGUMENT, this::universalArgument);
+        addBuiltinWidget(widgets, UNDO, this::undo);
+        addBuiltinWidget(widgets, UP_CASE_WORD, this::upCaseWord);
+        addBuiltinWidget(widgets, UP_HISTORY, this::upHistory);
+        addBuiltinWidget(widgets, UP_LINE, this::upLine);
+        addBuiltinWidget(widgets, UP_LINE_OR_HISTORY, this::upLineOrHistory);
+        addBuiltinWidget(widgets, UP_LINE_OR_SEARCH, this::upLineOrSearch);
+        addBuiltinWidget(widgets, VI_ADD_EOL, this::viAddEol);
+        addBuiltinWidget(widgets, VI_ADD_NEXT, this::viAddNext);
+        addBuiltinWidget(widgets, VI_BACKWARD_CHAR, this::viBackwardChar);
+        addBuiltinWidget(widgets, VI_BACKWARD_DELETE_CHAR, this::viBackwardDeleteChar);
+        addBuiltinWidget(widgets, VI_BACKWARD_BLANK_WORD, this::viBackwardBlankWord);
+        addBuiltinWidget(widgets, VI_BACKWARD_BLANK_WORD_END, this::viBackwardBlankWordEnd);
+        addBuiltinWidget(widgets, VI_BACKWARD_KILL_WORD, this::viBackwardKillWord);
+        addBuiltinWidget(widgets, VI_BACKWARD_WORD, this::viBackwardWord);
+        addBuiltinWidget(widgets, VI_BACKWARD_WORD_END, this::viBackwardWordEnd);
+        addBuiltinWidget(widgets, VI_BEGINNING_OF_LINE, this::viBeginningOfLine);
+        addBuiltinWidget(widgets, VI_CMD_MODE, this::viCmdMode);
+        addBuiltinWidget(widgets, VI_DIGIT_OR_BEGINNING_OF_LINE, this::viDigitOrBeginningOfLine);
+        addBuiltinWidget(widgets, VI_DOWN_LINE_OR_HISTORY, this::viDownLineOrHistory);
+        addBuiltinWidget(widgets, VI_CHANGE, this::viChange);
+        addBuiltinWidget(widgets, VI_CHANGE_EOL, this::viChangeEol);
+        addBuiltinWidget(widgets, VI_CHANGE_WHOLE_LINE, this::viChangeWholeLine);
+        addBuiltinWidget(widgets, VI_DELETE_CHAR, this::viDeleteChar);
+        addBuiltinWidget(widgets, VI_DELETE, this::viDelete);
+        addBuiltinWidget(widgets, VI_END_OF_LINE, this::viEndOfLine);
+        addBuiltinWidget(widgets, VI_KILL_EOL, this::viKillEol);
+        addBuiltinWidget(widgets, VI_FIRST_NON_BLANK, this::viFirstNonBlank);
+        addBuiltinWidget(widgets, VI_FIND_NEXT_CHAR, this::viFindNextChar);
+        addBuiltinWidget(widgets, VI_FIND_NEXT_CHAR_SKIP, this::viFindNextCharSkip);
+        addBuiltinWidget(widgets, VI_FIND_PREV_CHAR, this::viFindPrevChar);
+        addBuiltinWidget(widgets, VI_FIND_PREV_CHAR_SKIP, this::viFindPrevCharSkip);
+        addBuiltinWidget(widgets, VI_FORWARD_BLANK_WORD, this::viForwardBlankWord);
+        addBuiltinWidget(widgets, VI_FORWARD_BLANK_WORD_END, this::viForwardBlankWordEnd);
+        addBuiltinWidget(widgets, VI_FORWARD_CHAR, this::viForwardChar);
+        addBuiltinWidget(widgets, VI_FORWARD_WORD, this::viForwardWord);
+        addBuiltinWidget(widgets, VI_FORWARD_WORD, this::viForwardWord);
+        addBuiltinWidget(widgets, VI_FORWARD_WORD_END, this::viForwardWordEnd);
+        addBuiltinWidget(widgets, VI_HISTORY_SEARCH_BACKWARD, this::viHistorySearchBackward);
+        addBuiltinWidget(widgets, VI_HISTORY_SEARCH_FORWARD, this::viHistorySearchForward);
+        addBuiltinWidget(widgets, VI_INSERT, this::viInsert);
+        addBuiltinWidget(widgets, VI_INSERT_BOL, this::viInsertBol);
+        addBuiltinWidget(widgets, VI_INSERT_COMMENT, this::viInsertComment);
+        addBuiltinWidget(widgets, VI_JOIN, this::viJoin);
+        addBuiltinWidget(widgets, VI_KILL_LINE, this::viKillWholeLine);
+        addBuiltinWidget(widgets, VI_MATCH_BRACKET, this::viMatchBracket);
+        addBuiltinWidget(widgets, VI_OPEN_LINE_ABOVE, this::viOpenLineAbove);
+        addBuiltinWidget(widgets, VI_OPEN_LINE_BELOW, this::viOpenLineBelow);
+        addBuiltinWidget(widgets, VI_PUT_AFTER, this::viPutAfter);
+        addBuiltinWidget(widgets, VI_PUT_BEFORE, this::viPutBefore);
+        addBuiltinWidget(widgets, VI_REPEAT_FIND, this::viRepeatFind);
+        addBuiltinWidget(widgets, VI_REPEAT_SEARCH, this::viRepeatSearch);
+        addBuiltinWidget(widgets, VI_REPLACE_CHARS, this::viReplaceChars);
+        addBuiltinWidget(widgets, VI_REV_REPEAT_FIND, this::viRevRepeatFind);
+        addBuiltinWidget(widgets, VI_REV_REPEAT_SEARCH, this::viRevRepeatSearch);
+        addBuiltinWidget(widgets, VI_SWAP_CASE, this::viSwapCase);
+        addBuiltinWidget(widgets, VI_UP_LINE_OR_HISTORY, this::viUpLineOrHistory);
+        addBuiltinWidget(widgets, VI_YANK, this::viYankTo);
+        addBuiltinWidget(widgets, VI_YANK_WHOLE_LINE, this::viYankWholeLine);
+        addBuiltinWidget(widgets, VISUAL_LINE_MODE, this::visualLineMode);
+        addBuiltinWidget(widgets, VISUAL_MODE, this::visualMode);
+        addBuiltinWidget(widgets, WHAT_CURSOR_POSITION, this::whatCursorPosition);
+        addBuiltinWidget(widgets, YANK, this::yank);
+        addBuiltinWidget(widgets, YANK_POP, this::yankPop);
+        addBuiltinWidget(widgets, MOUSE, this::mouse);
+        addBuiltinWidget(widgets, BEGIN_PASTE, this::beginPaste);
+        addBuiltinWidget(widgets, FOCUS_IN, this::focusIn);
+        addBuiltinWidget(widgets, FOCUS_OUT, this::focusOut);
         return widgets;
     }
 
+    private void addBuiltinWidget(Map<String, Widget> widgets, String name, Widget widget) {
+        widgets.put(name, namedWidget(name, widget));
+    }
+
+    private Widget namedWidget(String name, Widget widget) {
+        return new Widget() {
+            @Override
+            public String toString() {
+                return name;
+            }
+            @Override
+            public boolean apply() {
+                return widget.apply();
+            }
+        };
+    }
+
     public boolean redisplay() {
         redisplay(true);
         return true;
     }
 
-    protected synchronized void redisplay(boolean flush) {
-        if (skipRedisplay) {
-            skipRedisplay = false;
-            return;
-        }
-
-        Status status = Status.getStatus(terminal, false);
-        if (status != null) {
-            status.redraw();
-        }
-
-        if (size.getRows() > 0 && size.getRows() < MIN_ROWS) {
-            AttributedStringBuilder sb = new AttributedStringBuilder().tabs(TAB_WIDTH);
-
-            sb.append(prompt);
-            concat(getHighlightedBuffer(buf.toString()).columnSplitLength(Integer.MAX_VALUE), sb);
-            AttributedString full = sb.toAttributedString();
-
-            sb.setLength(0);
-            sb.append(prompt);
-            String line = buf.upToCursor();
-            if(maskingCallback != null) {
-                line = maskingCallback.display(line);
-            }
-
-            concat(new AttributedString(line).columnSplitLength(Integer.MAX_VALUE), sb);
-            AttributedString toCursor = sb.toAttributedString();
-
-            int w = WCWidth.wcwidth('\u2026');
-            int width = size.getColumns();
-            int cursor = toCursor.columnLength();
-            int inc = width /2 + 1;
-            while (cursor <= smallTerminalOffset + w) {
-                smallTerminalOffset -= inc;
-            }
-            while (cursor >= smallTerminalOffset + width - w) {
-                smallTerminalOffset += inc;
-            }
-            if (smallTerminalOffset > 0) {
-                sb.setLength(0);
-                sb.append("\u2026");
-                sb.append(full.columnSubSequence(smallTerminalOffset + w, Integer.MAX_VALUE));
-                full = sb.toAttributedString();
-            }
-            int length = full.columnLength();
-            if (length >= smallTerminalOffset + width) {
+    protected void redisplay(boolean flush) {
+        try {
+            lock.lock();
+
+            if (skipRedisplay) {
+                skipRedisplay = false;
+                return;
+            }
+
+            Status status = Status.getStatus(terminal, false);
+            if (status != null) {
+                status.redraw();
+            }
+
+            if (size.getRows() > 0 && size.getRows() < MIN_ROWS) {
+                AttributedStringBuilder sb = new AttributedStringBuilder().tabs(TAB_WIDTH);
+
+                sb.append(prompt);
+                concat(getHighlightedBuffer(buf.toString()).columnSplitLength(Integer.MAX_VALUE), sb);
+                AttributedString full = sb.toAttributedString();
+
                 sb.setLength(0);
-                sb.append(full.columnSubSequence(0, width - w));
-                sb.append("\u2026");
-                full = sb.toAttributedString();
-            }
-
-            display.update(Collections.singletonList(full), cursor - smallTerminalOffset, flush);
-            return;
-        }
-
-        List<AttributedString> secondaryPrompts = new ArrayList<>();
-        AttributedString full = getDisplayedBufferWithPrompts(secondaryPrompts);
-
-        List<AttributedString> newLines;
-        if (size.getColumns() <= 0) {
-            newLines = new ArrayList<>();
-            newLines.add(full);
-        } else {
-            newLines = full.columnSplitLength(size.getColumns(), true, display.delayLineWrap());
-        }
-
-        List<AttributedString> rightPromptLines;
-        if (rightPrompt.length() == 0 || size.getColumns() <= 0) {
-            rightPromptLines = new ArrayList<>();
-        } else {
-            rightPromptLines = rightPrompt.columnSplitLength(size.getColumns());
-        }
-        while (newLines.size() < rightPromptLines.size()) {
-            newLines.add(new AttributedString(""));
-        }
-        for (int i = 0; i < rightPromptLines.size(); i++) {
-            AttributedString line = rightPromptLines.get(i);
-            newLines.set(i, addRightPrompt(line, newLines.get(i)));
-        }
-
-        int cursorPos = -1;
-        if (size.getColumns() > 0) {
-            AttributedStringBuilder sb = new AttributedStringBuilder().tabs(TAB_WIDTH);
-            sb.append(prompt);
-            String buffer = buf.upToCursor();
-            if (maskingCallback != null) {
-                buffer = maskingCallback.display(buffer);
-            }
-            sb.append(insertSecondaryPrompts(new AttributedString(buffer), secondaryPrompts, false));
-            List<AttributedString> promptLines = sb.columnSplitLength(size.getColumns(), false, display.delayLineWrap());
-            if (!promptLines.isEmpty()) {
-                cursorPos = size.cursorPos(promptLines.size() - 1,
-                                           promptLines.get(promptLines.size() - 1).columnLength());
-            }
-        }
-
-        display.update(newLines, cursorPos, flush);
+                sb.append(prompt);
+                String line = buf.upToCursor();
+                if (maskingCallback != null) {
+                    line = maskingCallback.display(line);
+                }
+
+                concat(new AttributedString(line).columnSplitLength(Integer.MAX_VALUE), sb);
+                AttributedString toCursor = sb.toAttributedString();
+
+                int w = WCWidth.wcwidth('\u2026');
+                int width = size.getColumns();
+                int cursor = toCursor.columnLength();
+                int inc = width / 2 + 1;
+                while (cursor <= smallTerminalOffset + w) {
+                    smallTerminalOffset -= inc;
+                }
+                while (cursor >= smallTerminalOffset + width - w) {
+                    smallTerminalOffset += inc;
+                }
+                if (smallTerminalOffset > 0) {
+                    sb.setLength(0);
+                    sb.append("\u2026");
+                    sb.append(full.columnSubSequence(smallTerminalOffset + w, Integer.MAX_VALUE));
+                    full = sb.toAttributedString();
+                }
+                int length = full.columnLength();
+                if (length >= smallTerminalOffset + width) {
+                    sb.setLength(0);
+                    sb.append(full.columnSubSequence(0, width - w));
+                    sb.append("\u2026");
+                    full = sb.toAttributedString();
+                }
+
+                display.update(Collections.singletonList(full), cursor - smallTerminalOffset, flush);
+                return;
+            }
+
+            List<AttributedString> secondaryPrompts = new ArrayList<>();
+            AttributedString full = getDisplayedBufferWithPrompts(secondaryPrompts);
+
+            List<AttributedString> newLines;
+            if (size.getColumns() <= 0) {
+                newLines = new ArrayList<>();
+                newLines.add(full);
+            } else {
+                newLines = full.columnSplitLength(size.getColumns(), true, display.delayLineWrap());
+            }
+
+            List<AttributedString> rightPromptLines;
+            if (rightPrompt.length() == 0 || size.getColumns() <= 0) {
+                rightPromptLines = new ArrayList<>();
+            } else {
+                rightPromptLines = rightPrompt.columnSplitLength(size.getColumns());
+            }
+            while (newLines.size() < rightPromptLines.size()) {
+                newLines.add(new AttributedString(""));
+            }
+            for (int i = 0; i < rightPromptLines.size(); i++) {
+                AttributedString line = rightPromptLines.get(i);
+                newLines.set(i, addRightPrompt(line, newLines.get(i)));
+            }
+
+            int cursorPos = -1;
+            int cursorNewLinesId = -1;
+            int cursorColPos = -1;
+            if (size.getColumns() > 0) {
+                AttributedStringBuilder sb = new AttributedStringBuilder().tabs(TAB_WIDTH);
+                sb.append(prompt);
+                String buffer = buf.upToCursor();
+                if (maskingCallback != null) {
+                    buffer = maskingCallback.display(buffer);
+                }
+                sb.append(insertSecondaryPrompts(new AttributedString(buffer), secondaryPrompts, false));
+                List<AttributedString> promptLines = sb.columnSplitLength(size.getColumns(), false, display.delayLineWrap());
+                if (!promptLines.isEmpty()) {
+                    cursorNewLinesId = promptLines.size() - 1;
+                    cursorColPos = promptLines.get(promptLines.size() - 1).columnLength();
+                    cursorPos = size.cursorPos(cursorNewLinesId, cursorColPos);
+                }
+            }
+
+            List<AttributedString> newLinesToDisplay = new ArrayList<>();
+            int displaySize = size.getRows() - (status != null ? status.size() : 0);
+            if (newLines.size() > displaySize && !isTerminalDumb()) {
+                StringBuilder sb = new StringBuilder(">....");
+                // blanks are needed when displaying command completion candidate list
+                for (int i = sb.toString().length(); i < size.getColumns(); i++) {
+                    sb.append(" ");
+                }
+                AttributedString partialCommandInfo = new AttributedString(sb.toString());
+                int lineId = newLines.size() - displaySize + 1;
+                int endId = displaySize;
+                int startId = 1;
+                if (lineId  > cursorNewLinesId) {
+                    lineId = cursorNewLinesId;
+                    endId = displaySize - 1;
+                    startId = 0;
+                } else {
+                    newLinesToDisplay.add(partialCommandInfo);
+                }
+                int cursorRowPos = 0;
+                for (int i = startId; i < endId; i++) {
+                    if (cursorNewLinesId == lineId) {
+                        cursorRowPos = i;
+                    }
+                    newLinesToDisplay.add(newLines.get(lineId++));
+                }
+                if (startId == 0) {
+                    newLinesToDisplay.add(partialCommandInfo);
+                }
+                cursorPos = size.cursorPos(cursorRowPos, cursorColPos);
+            } else {
+                newLinesToDisplay = newLines;
+            }
+            display.update(newLinesToDisplay, cursorPos, flush);
+        } finally {
+            lock.unlock();
+        }
     }
 
     private void concat(List<AttributedString> lines, AttributedStringBuilder sb) {
@@ -3656,26 +3841,26 @@
                 decode: while (true) {
                     ch = pattern.charAt(i++);
                     switch (ch) {
-                       case '{':
-                       case '}':
-                           String str = sb.toString();
-                           AttributedString astr;
-                           if (!isHidden) {
-                               astr = AttributedString.fromAnsi(str);
-                               cols += astr.columnLength();
-                           } else {
-                               astr = new AttributedString(str, AttributedStyle.HIDDEN);
-                           }
-                           if (padPartIndex == parts.size()) {
-                               padPartString = sb;
-                               if (i < plen) {
-                                   sb = new StringBuilder();
-                               }
-                           } else {
-                               sb.setLength(0);
-                           }
-                           parts.add(astr);
-                           isHidden = ch == '{';
+                        case '{':
+                        case '}':
+                            String str = sb.toString();
+                            AttributedString astr;
+                            if (!isHidden) {
+                                astr = AttributedString.fromAnsi(str);
+                                cols += astr.columnLength();
+                            } else {
+                                astr = new AttributedString(str, AttributedStyle.HIDDEN);
+                            }
+                            if (padPartIndex == parts.size()) {
+                                padPartString = sb;
+                                if (i < plen) {
+                                    sb = new StringBuilder();
+                                }
+                            } else {
+                                sb.setLength(0);
+                            }
+                            parts.add(astr);
+                            isHidden = ch == '{';
                             break decode;
                         case '%':
                             sb.append(ch);
@@ -4048,113 +4233,117 @@
         if (matching.isEmpty()) {
             return false;
         }
-
-        // If we only need to display the list, do it now
-        if (lst == CompletionType.List) {
+        size.copy(terminal.getSize());
+        try {
+            // If we only need to display the list, do it now
+            if (lst == CompletionType.List) {
+                List<Candidate> possible = matching.entrySet().stream()
+                        .flatMap(e -> e.getValue().stream())
+                        .collect(Collectors.toList());
+                doList(possible, line.word(), false, line::escape);
+                return !possible.isEmpty();
+            }
+
+            // Check if there's a single possible match
+            Candidate completion = null;
+            // If there's a single possible completion
+            if (matching.size() == 1) {
+                completion = matching.values().stream().flatMap(Collection::stream)
+                        .findFirst().orElse(null);
+            }
+            // Or if RECOGNIZE_EXACT is set, try to find an exact match
+            else if (isSet(Option.RECOGNIZE_EXACT)) {
+                completion = matching.values().stream().flatMap(Collection::stream)
+                        .filter(Candidate::complete)
+                        .filter(c -> exact.test(c.value()))
+                        .findFirst().orElse(null);
+            }
+            // Complete and exit
+            if (completion != null && !completion.value().isEmpty()) {
+                if (prefix) {
+                    buf.backspace(line.rawWordCursor());
+                } else {
+                    buf.move(line.rawWordLength() - line.rawWordCursor());
+                    buf.backspace(line.rawWordLength());
+                }
+                buf.write(line.escape(completion.value(), completion.complete()));
+                if (completion.complete()) {
+                    if (buf.currChar() != ' ') {
+                        buf.write(" ");
+                    } else {
+                        buf.move(1);
+                    }
+                }
+                if (completion.suffix() != null) {
+                    redisplay();
+                    Binding op = readBinding(getKeys());
+                    if (op != null) {
+                        String chars = getString(REMOVE_SUFFIX_CHARS, DEFAULT_REMOVE_SUFFIX_CHARS);
+                        String ref = op instanceof Reference ? ((Reference) op).name() : null;
+                        if (SELF_INSERT.equals(ref) && chars.indexOf(getLastBinding().charAt(0)) >= 0
+                                || ACCEPT_LINE.equals(ref)) {
+                            buf.backspace(completion.suffix().length());
+                            if (getLastBinding().charAt(0) != ' ') {
+                                buf.write(' ');
+                            }
+                        }
+                        pushBackBinding(true);
+                    }
+                }
+                return true;
+            }
+
             List<Candidate> possible = matching.entrySet().stream()
                     .flatMap(e -> e.getValue().stream())
                     .collect(Collectors.toList());
-            doList(possible, line.word(), false, line::escape);
-            return !possible.isEmpty();
-        }
-
-        // Check if there's a single possible match
-        Candidate completion = null;
-        // If there's a single possible completion
-        if (matching.size() == 1) {
-            completion = matching.values().stream().flatMap(Collection::stream)
-                    .findFirst().orElse(null);
-        }
-        // Or if RECOGNIZE_EXACT is set, try to find an exact match
-        else if (isSet(Option.RECOGNIZE_EXACT)) {
-            completion = matching.values().stream().flatMap(Collection::stream)
-                    .filter(Candidate::complete)
-                    .filter(c -> exact.test(c.value()))
-                    .findFirst().orElse(null);
-        }
-        // Complete and exit
-        if (completion != null && !completion.value().isEmpty()) {
+
+            if (useMenu) {
+                buf.move(line.word().length() - line.wordCursor());
+                buf.backspace(line.word().length());
+                doMenu(possible, line.word(), line::escape);
+                return true;
+            }
+
+            // Find current word and move to end
+            String current;
             if (prefix) {
-                buf.backspace(line.rawWordCursor());
+                current = line.word().substring(0, line.wordCursor());
             } else {
+                current = line.word();
                 buf.move(line.rawWordLength() - line.rawWordCursor());
+            }
+            // Now, we need to find the unambiguous completion
+            // TODO: need to find common suffix
+            String commonPrefix = null;
+            for (String key : matching.keySet()) {
+                commonPrefix = commonPrefix == null ? key : getCommonStart(commonPrefix, key, caseInsensitive);
+            }
+            boolean hasUnambiguous = commonPrefix.startsWith(current) && !commonPrefix.equals(current);
+
+            if (hasUnambiguous) {
                 buf.backspace(line.rawWordLength());
-            }
-            buf.write(line.escape(completion.value(), completion.complete()));
-            if (completion.complete()) {
-                if (buf.currChar() != ' ') {
-                    buf.write(" ");
-                } else {
-                    buf.move(1);
+                buf.write(line.escape(commonPrefix, false));
+                current = commonPrefix;
+                if ((!isSet(Option.AUTO_LIST) && isSet(Option.AUTO_MENU))
+                        || (isSet(Option.AUTO_LIST) && isSet(Option.LIST_AMBIGUOUS))) {
+                    if (!nextBindingIsComplete()) {
+                        return true;
+                    }
                 }
             }
-            if (completion.suffix() != null) {
-                redisplay();
-                Binding op = readBinding(getKeys());
-                if (op != null) {
-                    String chars = getString(REMOVE_SUFFIX_CHARS, DEFAULT_REMOVE_SUFFIX_CHARS);
-                    String ref = op instanceof Reference ? ((Reference) op).name() : null;
-                    if (SELF_INSERT.equals(ref) && chars.indexOf(getLastBinding().charAt(0)) >= 0
-                            || ACCEPT_LINE.equals(ref)) {
-                        buf.backspace(completion.suffix().length());
-                        if (getLastBinding().charAt(0) != ' ') {
-                            buf.write(' ');
-                        }
-                    }
-                    pushBackBinding(true);
+            if (isSet(Option.AUTO_LIST)) {
+                if (!doList(possible, current, true, line::escape)) {
+                    return true;
                 }
             }
+            if (isSet(Option.AUTO_MENU)) {
+                buf.backspace(current.length());
+                doMenu(possible, line.word(), line::escape);
+            }
             return true;
-        }
-
-        List<Candidate> possible = matching.entrySet().stream()
-                .flatMap(e -> e.getValue().stream())
-                .collect(Collectors.toList());
-
-        if (useMenu) {
-            buf.move(line.word().length() - line.wordCursor());
-            buf.backspace(line.word().length());
-            doMenu(possible, line.word(), line::escape);
-            return true;
-        }
-
-        // Find current word and move to end
-        String current;
-        if (prefix) {
-            current = line.word().substring(0, line.wordCursor());
-        } else {
-            current = line.word();
-            buf.move(line.rawWordLength() - line.rawWordCursor());
-        }
-        // Now, we need to find the unambiguous completion
-        // TODO: need to find common suffix
-        String commonPrefix = null;
-        for (String key : matching.keySet()) {
-            commonPrefix = commonPrefix == null ? key : getCommonStart(commonPrefix, key, caseInsensitive);
-        }
-        boolean hasUnambiguous = commonPrefix.startsWith(current) && !commonPrefix.equals(current);
-
-        if (hasUnambiguous) {
-            buf.backspace(line.rawWordLength());
-            buf.write(line.escape(commonPrefix, false));
-            current = commonPrefix;
-            if ((!isSet(Option.AUTO_LIST) && isSet(Option.AUTO_MENU))
-                    || (isSet(Option.AUTO_LIST) && isSet(Option.LIST_AMBIGUOUS))) {
-                if (!nextBindingIsComplete()) {
-                    return true;
-                }
-            }
-        }
-        if (isSet(Option.AUTO_LIST)) {
-            if (!doList(possible, current, true, line::escape)) {
-                return true;
-            }
-        }
-        if (isSet(Option.AUTO_MENU)) {
-            buf.backspace(current.length());
-            doMenu(possible, line.word(), line::escape);
-        }
-        return true;
+        } finally {
+            size.copy(terminal.getBufferSize());
+        }
     }
 
     private CompletingParsedLine wrap(ParsedLine line) {
@@ -4534,7 +4723,7 @@
         if (listMax > 0 && possible.size() >= listMax
                 || lines >= size.getRows() - promptLines) {
             // prompt
-            post = () -> new AttributedString(getAppName() + ": do you wish to see to see all " + possible.size()
+            post = () -> new AttributedString(getAppName() + ": do you wish to see all " + possible.size()
                     + " possibilities (" + lines + " lines)?");
             redisplay(true);
             int c = readCharacter();
@@ -4586,7 +4775,7 @@
             }
             redisplay();
             // TODO: use a different keyMap ?
-            Binding b = bindingReader.readBinding(getKeys());
+            Binding b = doReadBinding(getKeys(), null);
             if (b instanceof Reference) {
                 String name = ((Reference) b).name();
                 if (BACKWARD_DELETE_CHAR.equals(name) || VI_BACKWARD_DELETE_CHAR.equals(name)) {
@@ -4731,7 +4920,7 @@
 
     @SuppressWarnings("unchecked")
     protected void toColumns(Object items, int width, int maxWidth, AttributedStringBuilder sb, Candidate selection, String completed, boolean rowsFirst, int[] out) {
-        if (maxWidth <= 0) {
+        if (maxWidth <= 0 || width <= 0) {
             return;
         }
         // This is a group
@@ -4985,7 +5174,9 @@
                 while (end < buf.length() && buf.atChar(end) != '\n') {
                     end++;
                 }
-                end++;
+                if (end < buf.length()) {
+                    end++;
+                }
             }
         }
         String killed = buf.substring(start, end);
@@ -5188,7 +5379,7 @@
         keyMap.bind(END_PASTE, BRACKETED_PASTE_END);
         StringBuilder sb = new StringBuilder();
         while (true) {
-            Object b = bindingReader.readBinding(keyMap);
+            Object b = doReadBinding(keyMap, null);
             if (b == END_PASTE) {
                 break;
             }
@@ -5227,6 +5418,11 @@
      */
     public boolean clearScreen() {
         if (terminal.puts(Capability.clear_screen)) {
+            // ConEMU extended fonts support
+            if (AbstractWindowsTerminal.TYPE_WINDOWS_CONEMU.equals(terminal.getType())
+                    && !Boolean.getBoolean("org.jline.terminal.conemu.disable-activate")) {
+                terminal.writer().write("\u001b[9999E");
+            }
             Status status = Status.getStatus(terminal, false);
             if (status != null) {
                 status.reset();
@@ -5358,6 +5554,7 @@
 
     public KeyMap<Binding> emacs() {
         KeyMap<Binding> emacs = new KeyMap<>();
+        bindKeys(emacs);
         bind(emacs, SET_MARK_COMMAND,                       ctrl('@'));
         bind(emacs, BEGINNING_OF_LINE,                      ctrl('A'));
         bind(emacs, BACKWARD_CHAR,                          ctrl('B'));
@@ -5372,6 +5569,7 @@
         bind(emacs, CLEAR_SCREEN,                           ctrl('L'));
         bind(emacs, ACCEPT_LINE,                            ctrl('M'));
         bind(emacs, DOWN_LINE_OR_HISTORY,                   ctrl('N'));
+        bind(emacs, ACCEPT_LINE_AND_DOWN_HISTORY,           ctrl('O'));
         bind(emacs, UP_LINE_OR_HISTORY,                     ctrl('P'));
         bind(emacs, HISTORY_INCREMENTAL_SEARCH_BACKWARD,    ctrl('R'));
         bind(emacs, HISTORY_INCREMENTAL_SEARCH_FORWARD,     ctrl('S'));
@@ -5415,6 +5613,7 @@
         bind(emacs, END_OF_HISTORY,                         alt('>'));
         bind(emacs, LIST_CHOICES,                           alt('?'));
         bind(emacs, DO_LOWERCASE_VERSION,                   range("^[A-^[Z"));
+        bind(emacs, ACCEPT_AND_HOLD,                        alt('a'));
         bind(emacs, BACKWARD_WORD,                          alt('b'));
         bind(emacs, CAPITALIZE_WORD,                        alt('c'));
         bind(emacs, KILL_WORD,                              alt('d'));
@@ -5439,6 +5638,7 @@
 
     public KeyMap<Binding> viInsertion() {
         KeyMap<Binding> viins = new KeyMap<>();
+        bindKeys(viins);
         bind(viins, SELF_INSERT,                            range("^@-^_"));
         bind(viins, LIST_CHOICES,                           ctrl('D'));
         bind(viins, SEND_BREAK,                             ctrl('G'));
@@ -5638,6 +5838,14 @@
         return KeyMap.key(terminal, capability);
     }
 
+    private void bindKeys(KeyMap<Binding> emacs) {
+        Widget beep = namedWidget("beep", this::beep);
+        Stream.of(Capability.values())
+                .filter(c -> c.name().startsWith("key_"))
+                .map(this::key)
+                .forEach(k -> bind(emacs, beep, k));
+    }
+
     private void bindArrowKeys(KeyMap<Binding> map) {
         bind(map, UP_LINE_OR_SEARCH,    key(Capability.key_up));
         bind(map, DOWN_LINE_OR_SEARCH,  key(Capability.key_down));