| /* |
| * Copyright (c) 2002-2020, 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. |
| * |
| * https://opensource.org/licenses/BSD-3-Clause |
| */ |
| package jdk.internal.org.jline.reader.impl; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.FileReader; |
| import java.io.FileWriter; |
| import java.io.Flushable; |
| import java.io.IOError; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InterruptedIOException; |
| import java.lang.reflect.Constructor; |
| import java.time.Instant; |
| import java.util.*; |
| import java.util.Map.Entry; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| 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; |
| import jdk.internal.org.jline.keymap.KeyMap; |
| import jdk.internal.org.jline.reader.*; |
| import jdk.internal.org.jline.reader.Parser.ParseContext; |
| import jdk.internal.org.jline.reader.impl.history.DefaultHistory; |
| import jdk.internal.org.jline.terminal.*; |
| 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; |
| import jdk.internal.org.jline.utils.Curses; |
| import jdk.internal.org.jline.utils.Display; |
| import jdk.internal.org.jline.utils.InfoCmp.Capability; |
| import jdk.internal.org.jline.utils.Levenshtein; |
| import jdk.internal.org.jline.utils.Log; |
| import jdk.internal.org.jline.utils.Status; |
| import jdk.internal.org.jline.utils.WCWidth; |
| |
| import static jdk.internal.org.jline.keymap.KeyMap.alt; |
| import static jdk.internal.org.jline.keymap.KeyMap.ctrl; |
| import static jdk.internal.org.jline.keymap.KeyMap.del; |
| import static jdk.internal.org.jline.keymap.KeyMap.esc; |
| import static jdk.internal.org.jline.keymap.KeyMap.range; |
| import static jdk.internal.org.jline.keymap.KeyMap.translate; |
| |
| /** |
| * A reader for terminal applications. It supports custom tab-completion, |
| * saveable command history, and command line editing. |
| * |
| * @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> |
| */ |
| @SuppressWarnings("StatementWithEmptyBody") |
| public class LineReaderImpl implements LineReader, Flushable |
| { |
| public static final char NULL_MASK = 0; |
| |
| public static final int TAB_WIDTH = 4; |
| |
| |
| public static final String DEFAULT_WORDCHARS = "*?_-.[]~=/&;!#$%^(){}<>"; |
| public static final String DEFAULT_REMOVE_SUFFIX_CHARS = " \t\n;&|"; |
| public static final String DEFAULT_COMMENT_BEGIN = "#"; |
| public static final String DEFAULT_SEARCH_TERMINATORS = "\033\012"; |
| public static final String DEFAULT_BELL_STYLE = ""; |
| public static final int DEFAULT_LIST_MAX = 100; |
| public static final int DEFAULT_ERRORS = 2; |
| public static final long DEFAULT_BLINK_MATCHING_PAREN = 500L; |
| public static final long DEFAULT_AMBIGUOUS_BINDING = 1000L; |
| public static final String DEFAULT_SECONDARY_PROMPT_PATTERN = "%M> "; |
| public static final String DEFAULT_OTHERS_GROUP_NAME = "others"; |
| public static final String DEFAULT_ORIGINAL_GROUP_NAME = "original"; |
| public static final String DEFAULT_COMPLETION_STYLE_STARTING = "36"; // cyan |
| public static final String DEFAULT_COMPLETION_STYLE_DESCRIPTION = "90"; // dark gray |
| public static final String DEFAULT_COMPLETION_STYLE_GROUP = "35;1"; // magenta |
| public static final String DEFAULT_COMPLETION_STYLE_SELECTION = "7"; // inverted |
| public static final int DEFAULT_INDENTATION = 0; |
| public static final int DEFAULT_FEATURES_MAX_BUFFER_SIZE = 1000; |
| |
| private static final int MIN_ROWS = 3; |
| |
| public static final String BRACKETED_PASTE_ON = "\033[?2004h"; |
| public static final String BRACKETED_PASTE_OFF = "\033[?2004l"; |
| public static final String BRACKETED_PASTE_BEGIN = "\033[200~"; |
| public static final String BRACKETED_PASTE_END = "\033[201~"; |
| |
| public static final String FOCUS_IN_SEQ = "\033[I"; |
| public static final String FOCUS_OUT_SEQ = "\033[O"; |
| |
| /** |
| * Possible states in which the current readline operation may be in. |
| */ |
| protected enum State { |
| /** |
| * The user is just typing away |
| */ |
| NORMAL, |
| /** |
| * readLine should exit and return the buffer content |
| */ |
| DONE, |
| /** |
| * readLine should exit and return empty String |
| */ |
| IGNORE, |
| /** |
| * readLine should exit and throw an EOFException |
| */ |
| EOF, |
| /** |
| * readLine should exit and throw an UserInterruptException |
| */ |
| INTERRUPT |
| } |
| |
| protected enum ViMoveMode { |
| NORMAL, |
| YANK, |
| DELETE, |
| CHANGE |
| } |
| |
| protected enum BellType { |
| NONE, |
| AUDIBLE, |
| VISIBLE |
| } |
| |
| // |
| // Constructor variables |
| // |
| |
| /** The terminal to use */ |
| protected final Terminal terminal; |
| /** The application name */ |
| protected final String appName; |
| /** The terminal keys mapping */ |
| protected final Map<String, KeyMap<Binding>> keyMaps; |
| |
| // |
| // Configuration |
| // |
| protected final Map<String, Object> variables; |
| protected History history = new DefaultHistory(); |
| protected Completer completer = null; |
| protected Highlighter highlighter = new DefaultHighlighter(); |
| protected Parser parser = new DefaultParser(); |
| protected Expander expander = new DefaultExpander(); |
| |
| // |
| // State variables |
| // |
| |
| protected final Map<Option, Boolean> options = new HashMap<>(); |
| |
| protected final Buffer buf = new BufferImpl(); |
| protected String tailTip = ""; |
| protected SuggestionType autosuggestion = SuggestionType.NONE; |
| |
| protected final Size size = new Size(); |
| |
| protected AttributedString prompt = AttributedString.EMPTY; |
| protected AttributedString rightPrompt = AttributedString.EMPTY; |
| |
| protected MaskingCallback maskingCallback; |
| |
| protected Map<Integer, String> modifiedHistory = new HashMap<>(); |
| protected Buffer historyBuffer = null; |
| protected CharSequence searchBuffer; |
| protected StringBuffer searchTerm = null; |
| protected boolean searchFailing; |
| protected boolean searchBackward; |
| protected int searchIndex = -1; |
| protected boolean doAutosuggestion; |
| |
| |
| // Reading buffers |
| protected final BindingReader bindingReader; |
| |
| |
| /** |
| * VI character find |
| */ |
| protected int findChar; |
| protected int findDir; |
| protected int findTailAdd; |
| /** |
| * VI history string search |
| */ |
| private int searchDir; |
| private String searchString; |
| |
| /** |
| * Region state |
| */ |
| protected int regionMark; |
| protected RegionType regionActive; |
| |
| private boolean forceChar; |
| private boolean forceLine; |
| |
| /** |
| * The vi yank buffer |
| */ |
| protected String yankBuffer = ""; |
| |
| protected ViMoveMode viMoveMode = ViMoveMode.NORMAL; |
| |
| protected KillRing killRing = new KillRing(); |
| |
| 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 |
| */ |
| protected State state = State.DONE; |
| protected final AtomicBoolean startedReading = new AtomicBoolean(); |
| protected boolean reading; |
| |
| protected Supplier<AttributedString> post; |
| |
| protected Map<String, Widget> builtinWidgets; |
| protected Map<String, Widget> widgets; |
| |
| protected int count; |
| protected int mult; |
| protected int universal = 4; |
| protected int repeatCount; |
| protected boolean isArgDigit; |
| |
| protected ParsedLine parsedLine; |
| |
| protected boolean skipRedisplay; |
| protected Display display; |
| |
| protected boolean overTyping = false; |
| |
| protected String keyMap; |
| |
| 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; |
| |
| /* |
| * execute commands from commandsBuffer |
| */ |
| protected List<String> commandsBuffer = new ArrayList<>(); |
| |
| public LineReaderImpl(Terminal terminal) throws IOException { |
| this(terminal, null, null); |
| } |
| |
| public LineReaderImpl(Terminal terminal, String appName) throws IOException { |
| this(terminal, appName, null); |
| } |
| |
| public LineReaderImpl(Terminal terminal, String appName, Map<String, Object> variables) { |
| Objects.requireNonNull(terminal, "terminal can not be null"); |
| this.terminal = terminal; |
| if (appName == null) { |
| appName = "JLine"; |
| } |
| this.appName = appName; |
| if (variables != null) { |
| this.variables = variables; |
| } else { |
| this.variables = new HashMap<>(); |
| } |
| this.keyMaps = defaultKeyMaps(); |
| |
| builtinWidgets = builtinWidgets(); |
| widgets = new HashMap<>(builtinWidgets); |
| bindingReader = new BindingReader(terminal.reader()); |
| doDisplay(); |
| } |
| |
| public Terminal getTerminal() { |
| return terminal; |
| } |
| |
| public String getAppName() { |
| return appName; |
| } |
| |
| public Map<String, KeyMap<Binding>> getKeyMaps() { |
| return keyMaps; |
| } |
| |
| public KeyMap<Binding> getKeys() { |
| return keyMaps.get(keyMap); |
| } |
| |
| @Override |
| public Map<String, Widget> getWidgets() { |
| return widgets; |
| } |
| |
| @Override |
| public Map<String, Widget> getBuiltinWidgets() { |
| return Collections.unmodifiableMap(builtinWidgets); |
| } |
| |
| @Override |
| public Buffer getBuffer() { |
| return buf; |
| } |
| |
| @Override |
| public void setAutosuggestion(SuggestionType type) { |
| this.autosuggestion = type; |
| } |
| |
| @Override |
| public SuggestionType getAutosuggestion() { |
| return autosuggestion; |
| } |
| |
| @Override |
| public String getTailTip() { |
| return tailTip; |
| } |
| |
| @Override |
| public void setTailTip(String tailTip) { |
| this.tailTip = tailTip; |
| } |
| |
| @Override |
| public void runMacro(String macro) { |
| bindingReader.runMacro(macro); |
| } |
| |
| @Override |
| public MouseEvent readMouseEvent() { |
| return terminal.readMouseEvent(bindingReader::readCharacter); |
| } |
| |
| /** |
| * Set the completer. |
| * |
| * @param completer the completer to use |
| */ |
| public void setCompleter(Completer completer) { |
| this.completer = completer; |
| } |
| |
| /** |
| * Returns the completer. |
| * |
| * @return the completer |
| */ |
| public Completer getCompleter() { |
| return completer; |
| } |
| |
| // |
| // History |
| // |
| |
| public void setHistory(final History history) { |
| Objects.requireNonNull(history); |
| this.history = history; |
| } |
| |
| public History getHistory() { |
| return history; |
| } |
| |
| // |
| // Highlighter |
| // |
| |
| public void setHighlighter(Highlighter highlighter) { |
| this.highlighter = highlighter; |
| } |
| |
| public Highlighter getHighlighter() { |
| return highlighter; |
| } |
| |
| public Parser getParser() { |
| return parser; |
| } |
| |
| public void setParser(Parser parser) { |
| this.parser = parser; |
| } |
| |
| @Override |
| public Expander getExpander() { |
| return expander; |
| } |
| |
| public void setExpander(Expander expander) { |
| this.expander = expander; |
| } |
| |
| // |
| // Line Reading |
| // |
| |
| /** |
| * Read the next line and return the contents of the buffer. |
| * |
| * @return A line that is read from the terminal, can never be null. |
| */ |
| public String readLine() throws UserInterruptException, EndOfFileException { |
| return readLine(null, null, (MaskingCallback) null, 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. |
| * |
| * @param mask The mask character, <code>null</code> or <code>0</code>. |
| * @return A line that is read from the terminal, can never be null. |
| */ |
| public String readLine(Character mask) throws UserInterruptException, EndOfFileException { |
| return readLine(null, null, mask, null); |
| } |
| |
| /** |
| * 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 terminal, may be null. |
| * @return A line that is read from the terminal, can never be null. |
| */ |
| public String readLine(String prompt) throws UserInterruptException, EndOfFileException { |
| return readLine(prompt, null, (MaskingCallback) null, null); |
| } |
| |
| /** |
| * 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 terminal, may be null. |
| * @param mask The mask character, <code>null</code> or <code>0</code>. |
| * @return A line that is read from the terminal, can never be null. |
| */ |
| public String readLine(String prompt, Character mask) throws UserInterruptException, EndOfFileException { |
| return readLine(prompt, null, mask, null); |
| } |
| |
| /** |
| * 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 terminal, may be null. |
| * @param mask The mask character, <code>null</code> or <code>0</code>. |
| * @param buffer A string that will be set for editing. |
| * @return A line that is read from the terminal, can never be null. |
| */ |
| public String readLine(String prompt, Character mask, String buffer) throws UserInterruptException, EndOfFileException { |
| return readLine(prompt, null, mask, buffer); |
| } |
| |
| /** |
| * 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 terminal, may be null. |
| * @param rightPrompt The prompt to issue to the right of the terminal, may be null. |
| * @param mask The mask character, <code>null</code> or <code>0</code>. |
| * @param buffer A string that will be set for editing. |
| * @return A line that is read from the terminal, can never be null. |
| */ |
| public String readLine(String prompt, String rightPrompt, Character mask, String buffer) throws UserInterruptException, EndOfFileException { |
| return readLine(prompt, rightPrompt, mask != null ? new SimpleMaskingCallback(mask) : null, buffer); |
| } |
| |
| /** |
| * 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 terminal, may be null. |
| * @param rightPrompt The prompt to issue to the right of the terminal, may be null. |
| * @param maskingCallback The callback used to mask parts of the edited line. |
| * @param buffer A string that will be set for editing. |
| * @return A line that is read from the terminal, can never be null. |
| */ |
| public String readLine(String prompt, String rightPrompt, MaskingCallback maskingCallback, String buffer) throws UserInterruptException, EndOfFileException { |
| // prompt may be null |
| // maskingCallback may be null |
| // buffer may be null |
| if (!commandsBuffer.isEmpty()) { |
| String cmd = commandsBuffer.remove(0); |
| boolean done = false; |
| do { |
| try { |
| parser.parse(cmd, cmd.length() + 1, ParseContext.ACCEPT_LINE); |
| done = true; |
| } catch (EOFError e) { |
| if (commandsBuffer.isEmpty()) { |
| throw new IllegalArgumentException("Incompleted command: \n" + cmd); |
| } |
| cmd += "\n"; |
| cmd += commandsBuffer.remove(0); |
| } catch (SyntaxError e) { |
| done = true; |
| } catch (Exception e) { |
| commandsBuffer.clear(); |
| throw new IllegalArgumentException(e.getMessage()); |
| } |
| } while (!done); |
| AttributedStringBuilder sb = new AttributedStringBuilder(); |
| sb.styled(AttributedStyle::bold, cmd); |
| sb.toAttributedString().println(terminal); |
| terminal.flush(); |
| return finish(cmd); |
| } |
| |
| if (!startedReading.compareAndSet(false, true)) { |
| throw new IllegalStateException(); |
| } |
| |
| Thread readLineThread = Thread.currentThread(); |
| SignalHandler previousIntrHandler = null; |
| SignalHandler previousWinchHandler = null; |
| SignalHandler previousContHandler = null; |
| Attributes originalAttributes = null; |
| boolean dumb = isTerminalDumb(); |
| try { |
| |
| this.maskingCallback = maskingCallback; |
| |
| /* |
| * 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. |
| */ |
| repeatCount = 0; |
| mult = 1; |
| regionActive = RegionType.NONE; |
| regionMark = -1; |
| |
| smallTerminalOffset = 0; |
| |
| state = State.NORMAL; |
| |
| modifiedHistory.clear(); |
| |
| setPrompt(prompt); |
| setRightPrompt(rightPrompt); |
| buf.clear(); |
| 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; |
| |
| if (history != null) { |
| history.attach(this); |
| } |
| |
| try { |
| lock.lock(); |
| |
| this.reading = true; |
| |
| previousIntrHandler = terminal.handle(Signal.INT, signal -> readLineThread.interrupt()); |
| previousWinchHandler = terminal.handle(Signal.WINCH, this::handleSignal); |
| previousContHandler = terminal.handle(Signal.CONT, this::handleSignal); |
| originalAttributes = terminal.enterRawMode(); |
| |
| doDisplay(); |
| |
| // Move into application mode |
| if (!dumb) { |
| terminal.puts(Capability.keypad_xmit); |
| if (isSet(Option.AUTO_FRESH_LINE)) |
| callWidget(FRESH_LINE); |
| if (isSet(Option.MOUSE)) |
| terminal.trackMouse(Terminal.MouseTracking.Normal); |
| if (isSet(Option.BRACKETED_PASTE)) |
| terminal.writer().write(BRACKETED_PASTE_ON); |
| } else { |
| // For dumb terminals, we need to make sure that CR are ignored |
| Attributes attr = new Attributes(originalAttributes); |
| attr.setInputFlag(Attributes.InputFlag.IGNCR, true); |
| terminal.setAttributes(attr); |
| } |
| |
| callWidget(CALLBACK_INIT); |
| |
| undo.newState(buf.copy()); |
| |
| // Draw initial prompt |
| redrawLine(); |
| redisplay(); |
| } finally { |
| lock.unlock(); |
| } |
| |
| while (true) { |
| |
| KeyMap<Binding> local = null; |
| if (isInViCmdMode() && regionActive != RegionType.NONE) { |
| local = keyMaps.get(VISUAL); |
| } |
| Binding o = readBinding(getKeys(), local); |
| if (o == null) { |
| throw new EndOfFileException(); |
| } |
| Log.trace("Binding: ", o); |
| if (buf.length() == 0 && getLastBinding().charAt(0) == originalAttributes.getControlChar(ControlChar.VEOF)) { |
| throw new EndOfFileException(); |
| } |
| |
| // If this is still false after handling the binding, then |
| // we reset our repeatCount to 0. |
| 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. |
| count = ((repeatCount == 0) ? 1 : repeatCount) * mult; |
| // Reset undo/redo flag |
| isUndo = false; |
| // Reset region after a paste |
| if (regionActive == RegionType.PASTE) { |
| regionActive = RegionType.NONE; |
| } |
| |
| try { |
| lock.lock(); |
| // Get executable widget |
| Buffer copy = buf.length() <= getInt(FEATURES_MAX_BUFFER_SIZE, DEFAULT_FEATURES_MAX_BUFFER_SIZE) ? buf.copy() : null; |
| Widget w = getWidget(o); |
| if (!w.apply()) { |
| beep(); |
| } |
| if (!isUndo && copy != null && buf.length() <= getInt(FEATURES_MAX_BUFFER_SIZE, DEFAULT_FEATURES_MAX_BUFFER_SIZE) |
| && !copy.toString().equals(buf.toString())) { |
| undo.newState(buf.copy()); |
| } |
| |
| switch (state) { |
| case DONE: |
| return finishBuffer(); |
| case IGNORE: |
| return ""; |
| case EOF: |
| throw new EndOfFileException(); |
| case INTERRUPT: |
| throw new UserInterruptException(buf.toString()); |
| } |
| |
| if (!isArgDigit) { |
| /* |
| * If the operation performed wasn't a vi argument |
| * digit, then clear out the current repeatCount; |
| */ |
| repeatCount = 0; |
| mult = 1; |
| } |
| |
| if (!dumb) { |
| redisplay(); |
| } |
| } finally { |
| lock.unlock(); |
| } |
| } |
| } catch (IOError e) { |
| if (e.getCause() instanceof InterruptedIOException) { |
| throw new UserInterruptException(buf.toString()); |
| } else { |
| throw e; |
| } |
| } |
| finally { |
| try { |
| lock.lock(); |
| |
| this.reading = false; |
| |
| cleanup(); |
| if (originalAttributes != null) { |
| terminal.setAttributes(originalAttributes); |
| } |
| if (previousIntrHandler != null) { |
| terminal.handle(Signal.INT, previousIntrHandler); |
| } |
| if (previousWinchHandler != null) { |
| terminal.handle(Signal.WINCH, previousWinchHandler); |
| } |
| 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 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 |
| public void printAbove(AttributedString str) { |
| printAbove(str.toAnsi(terminal)); |
| } |
| |
| @Override |
| public boolean isReading() { |
| try { |
| lock.lock(); |
| return reading; |
| } finally { |
| lock.unlock(); |
| } |
| } |
| |
| /* Make sure we position the cursor on column 0 */ |
| protected boolean freshLine() { |
| boolean wrapAtEol = terminal.getBooleanCapability(Capability.auto_right_margin); |
| boolean delayedWrapAtEol = wrapAtEol && terminal.getBooleanCapability(Capability.eat_newline_glitch); |
| AttributedStringBuilder sb = new AttributedStringBuilder(); |
| sb.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.BLACK + AttributedStyle.BRIGHT)); |
| sb.append("~"); |
| sb.style(AttributedStyle.DEFAULT); |
| if (!wrapAtEol || delayedWrapAtEol) { |
| for (int i = 0; i < size.getColumns() - 1; i++) { |
| sb.append(" "); |
| } |
| sb.append(KeyMap.key(terminal, Capability.carriage_return)); |
| sb.append(" "); |
| sb.append(KeyMap.key(terminal, Capability.carriage_return)); |
| } else { |
| // Given the terminal will wrap automatically, |
| // we need to print one less than needed. |
| // This means that the last character will not |
| // be overwritten, and that's why we're using |
| // a clr_eol first if possible. |
| String el = terminal.getStringCapability(Capability.clr_eol); |
| if (el != null) { |
| Curses.tputs(sb, el); |
| } |
| for (int i = 0; i < size.getColumns() - 2; i++) { |
| sb.append(" "); |
| } |
| sb.append(KeyMap.key(terminal, Capability.carriage_return)); |
| sb.append(" "); |
| sb.append(KeyMap.key(terminal, Capability.carriage_return)); |
| } |
| sb.print(terminal); |
| return true; |
| } |
| |
| @Override |
| public void callWidget(String name) { |
| try { |
| 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(); |
| } |
| } |
| |
| /** |
| * Clear the line and redraw it. |
| * @return <code>true</code> |
| */ |
| public boolean redrawLine() { |
| display.reset(); |
| return true; |
| } |
| |
| /** |
| * Write out the specified string to the buffer and the output stream. |
| * @param str the char sequence to write in the buffer |
| */ |
| public void putString(final CharSequence str) { |
| buf.write(str, overTyping); |
| } |
| |
| /** |
| * Flush the terminal output stream. This is important for printout out single |
| * characters (like a buf.backspace or keyboard) that we want the terminal to |
| * handle immediately. |
| */ |
| public void flush() { |
| terminal.flush(); |
| } |
| |
| public boolean isKeyMap(String name) { |
| return keyMap.equals(name); |
| } |
| |
| /** |
| * Read a character from the terminal. |
| * |
| * @return the character, or -1 if an EOF is received. |
| */ |
| public int 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); |
| } |
| } |
| |
| protected String doReadStringUntil(String sequence) { |
| if (lock.isHeldByCurrentThread()) { |
| try { |
| lock.unlock(); |
| return bindingReader.readStringUntil(sequence); |
| } finally { |
| lock.lock(); |
| } |
| } else { |
| return bindingReader.readStringUntil(sequence); |
| } |
| } |
| |
| /** |
| * Read from the input stream and decode an operation from the key map. |
| * |
| * The input stream will be read character by character until a matching |
| * binding can be found. Characters that can't possibly be matched to |
| * any binding will be discarded. |
| * |
| * @param keys the KeyMap to use for decoding the input stream |
| * @return the decoded binding or <code>null</code> if the end of |
| * stream has been reached |
| */ |
| public Binding readBinding(KeyMap<Binding> keys) { |
| return readBinding(keys, null); |
| } |
| |
| public Binding readBinding(KeyMap<Binding> keys, KeyMap<Binding> 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 |
| * that state here if needed. |
| */ |
| if (o instanceof Reference) { |
| String ref = ((Reference) o).name(); |
| if (!YANK_POP.equals(ref) && !YANK.equals(ref)) { |
| killRing.resetLastYank(); |
| } |
| if (!KILL_LINE.equals(ref) && !KILL_WHOLE_LINE.equals(ref) |
| && !BACKWARD_KILL_WORD.equals(ref) && !KILL_WORD.equals(ref)) { |
| killRing.resetLastKill(); |
| } |
| } |
| return o; |
| } |
| |
| @Override |
| public ParsedLine getParsedLine() { |
| return parsedLine; |
| } |
| |
| @Override |
| public String getLastBinding() { |
| return bindingReader.getLastBinding(); |
| } |
| |
| @Override |
| public String getSearchTerm() { |
| return searchTerm != null ? searchTerm.toString() : null; |
| } |
| |
| @Override |
| public RegionType getRegionActive() { |
| return regionActive; |
| } |
| |
| @Override |
| public int getRegionMark() { |
| return regionMark; |
| } |
| |
| // |
| // Key Bindings |
| // |
| |
| /** |
| * Sets the current keymap by name. Supported keymaps are "emacs", |
| * "viins", "vicmd". |
| * @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) { |
| KeyMap<Binding> map = keyMaps.get(name); |
| if (map == null) { |
| return false; |
| } |
| this.keyMap = name; |
| if (reading) { |
| callWidget(CALLBACK_KEYMAP); |
| } |
| return true; |
| } |
| |
| /** |
| * 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 keyMap; |
| } |
| |
| @Override |
| public LineReader variable(String name, Object value) { |
| variables.put(name, value); |
| return this; |
| } |
| |
| @Override |
| public Map<String, Object> getVariables() { |
| return variables; |
| } |
| |
| @Override |
| public Object getVariable(String name) { |
| return variables.get(name); |
| } |
| |
| @Override |
| public void setVariable(String name, Object value) { |
| variables.put(name, value); |
| } |
| |
| @Override |
| public LineReader option(Option option, boolean value) { |
| options.put(option, value); |
| return this; |
| } |
| |
| @Override |
| public boolean isSet(Option option) { |
| Boolean b = options.get(option); |
| return b != null ? b : option.isDef(); |
| } |
| |
| @Override |
| public void setOpt(Option option) { |
| options.put(option, Boolean.TRUE); |
| } |
| |
| @Override |
| public void unsetOpt(Option option) { |
| options.put(option, Boolean.FALSE); |
| } |
| |
| @Override |
| public void addCommandsInBuffer(Collection<String> commands) { |
| commandsBuffer.addAll(commands); |
| } |
| |
| @Override |
| public void editAndAddInBuffer(File file) throws Exception { |
| Constructor<?> ctor = Class.forName("org.jline.builtins.Nano").getConstructor(Terminal.class, File.class); |
| Editor editor = (Editor) ctor.newInstance(terminal, new File(file.getParent())); |
| editor.setRestricted(true); |
| editor.open(Arrays.asList(file.getName())); |
| editor.run(); |
| BufferedReader br = new BufferedReader(new FileReader(file)); |
| String line; |
| commandsBuffer.clear(); |
| while ((line = br.readLine()) != null) { |
| commandsBuffer.add(line); |
| } |
| br.close(); |
| } |
| |
| // |
| // Widget implementation |
| // |
| |
| /** |
| * Clear the buffer and add its contents to the history. |
| * |
| * @return the former contents of the buffer. |
| */ |
| protected String finishBuffer() { |
| return finish(buf.toString()); |
| } |
| |
| protected String finish(String str) { |
| String historyLine = str; |
| |
| if (!isSet(Option.DISABLE_EVENT_EXPANSION)) { |
| StringBuilder sb = new StringBuilder(); |
| boolean escaped = false; |
| for (int i = 0; i < str.length(); i++) { |
| char ch = str.charAt(i); |
| if (escaped) { |
| escaped = false; |
| if (ch != '\n') { |
| sb.append(ch); |
| } |
| } else if (parser.isEscapeChar(ch)) { |
| escaped = true; |
| } else { |
| sb.append(ch); |
| } |
| } |
| str = sb.toString(); |
| } |
| |
| if (maskingCallback != null) { |
| historyLine = maskingCallback.history(historyLine); |
| } |
| |
| // we only add it to the history if the buffer is not empty |
| if (historyLine != null && historyLine.length() > 0 ) { |
| history.add(Instant.now(), historyLine); |
| } |
| return str; |
| } |
| |
| protected void handleSignal(Signal signal) { |
| doAutosuggestion = false; |
| if (signal == Signal.WINCH) { |
| Status status = Status.getStatus(terminal, false); |
| if (status != null) { |
| status.hardReset(); |
| } |
| size.copy(terminal.getBufferSize()); |
| display.resize(size.getRows(), size.getColumns()); |
| // restores prompt but also prevents scrolling in consoleZ, see #492 |
| // redrawLine(); |
| redisplay(); |
| } |
| else if (signal == Signal.CONT) { |
| terminal.enterRawMode(); |
| size.copy(terminal.getBufferSize()); |
| display.resize(size.getRows(), size.getColumns()); |
| terminal.puts(Capability.keypad_xmit); |
| redrawLine(); |
| redisplay(); |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| protected Widget getWidget(Object binding) { |
| Widget w; |
| if (binding instanceof Widget) { |
| w = (Widget) binding; |
| } else if (binding instanceof Macro) { |
| String macro = ((Macro) binding).getSequence(); |
| w = () -> { |
| bindingReader.runMacro(macro); |
| return true; |
| }; |
| } else if (binding instanceof Reference) { |
| String name = ((Reference) binding).name(); |
| w = widgets.get(name); |
| if (w == null) { |
| w = () -> { |
| post = () -> new AttributedString("No such widget `" + name + "'"); |
| return false; |
| }; |
| } |
| } else { |
| w = () -> { |
| post = () -> new AttributedString("Unsupported widget"); |
| return false; |
| }; |
| } |
| return w; |
| } |
| |
| // |
| // Helper methods |
| // |
| |
| public void setPrompt(final String prompt) { |
| this.prompt = (prompt == null ? AttributedString.EMPTY |
| : expandPromptPattern(prompt, 0, "", 0)); |
| } |
| |
| public void setRightPrompt(final String rightPrompt) { |
| this.rightPrompt = (rightPrompt == null ? AttributedString.EMPTY |
| : expandPromptPattern(rightPrompt, 0, "", 0)); |
| } |
| |
| protected void setBuffer(Buffer buffer) { |
| buf.copyFrom(buffer); |
| } |
| |
| /** |
| * Set the current buffer's content to the specified {@link String}. The |
| * visual terminal will be modified to show the current buffer. |
| * |
| * @param buffer the new contents of the buffer. |
| */ |
| protected void setBuffer(final String buffer) { |
| buf.clear(); |
| buf.write(buffer); |
| } |
| |
| /** |
| * 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 |
| */ |
| protected String viDeleteChangeYankToRemap (String op) { |
| switch (op) { |
| case SEND_BREAK: |
| case BACKWARD_CHAR: |
| case FORWARD_CHAR: |
| case END_OF_LINE: |
| case VI_MATCH_BRACKET: |
| case VI_DIGIT_OR_BEGINNING_OF_LINE: |
| case NEG_ARGUMENT: |
| case DIGIT_ARGUMENT: |
| case VI_BACKWARD_CHAR: |
| case VI_BACKWARD_WORD: |
| case VI_FORWARD_CHAR: |
| case VI_FORWARD_WORD: |
| case VI_FORWARD_WORD_END: |
| case VI_FIRST_NON_BLANK: |
| case VI_GOTO_COLUMN: |
| case VI_DELETE: |
| case VI_YANK: |
| case VI_CHANGE: |
| case VI_FIND_NEXT_CHAR: |
| case VI_FIND_NEXT_CHAR_SKIP: |
| case VI_FIND_PREV_CHAR: |
| case VI_FIND_PREV_CHAR_SKIP: |
| case VI_REPEAT_FIND: |
| case VI_REV_REPEAT_FIND: |
| return op; |
| |
| default: |
| return VI_CMD_MODE; |
| } |
| } |
| |
| protected int switchCase(int ch) { |
| if (Character.isUpperCase(ch)) { |
| return Character.toLowerCase(ch); |
| } else if (Character.isLowerCase(ch)) { |
| return Character.toUpperCase(ch); |
| } else { |
| return ch; |
| } |
| } |
| |
| /** |
| * @return true if line reader is in the middle of doing a change-to |
| * delete-to or yank-to. |
| */ |
| protected boolean isInViMoveOperation() { |
| return viMoveMode != ViMoveMode.NORMAL; |
| } |
| |
| protected boolean isInViChangeOperation() { |
| return viMoveMode == ViMoveMode.CHANGE; |
| } |
| |
| protected boolean isInViCmdMode() { |
| return VICMD.equals(keyMap); |
| } |
| |
| |
| // |
| // Movement |
| // |
| |
| protected boolean viForwardChar() { |
| if (count < 0) { |
| return callNeg(this::viBackwardChar); |
| } |
| int lim = findeol(); |
| if (isInViCmdMode() && !isInViMoveOperation()) { |
| lim--; |
| } |
| if (buf.cursor() >= lim) { |
| return false; |
| } |
| while (count-- > 0 && buf.cursor() < lim) { |
| buf.move(1); |
| } |
| return true; |
| } |
| |
| protected boolean viBackwardChar() { |
| if (count < 0) { |
| return callNeg(this::viForwardChar); |
| } |
| int lim = findbol(); |
| if (buf.cursor() == lim) { |
| return false; |
| } |
| while (count-- > 0 && buf.cursor() > 0) { |
| buf.move(-1); |
| if (buf.currChar() == '\n') { |
| buf.move(1); |
| break; |
| } |
| } |
| return true; |
| } |
| |
| |
| // |
| // Word movement |
| // |
| |
| protected boolean forwardWord() { |
| if (count < 0) { |
| return callNeg(this::backwardWord); |
| } |
| while (count-- > 0) { |
| while (buf.cursor() < buf.length() && isWord(buf.currChar())) { |
| buf.move(1); |
| } |
| if (isInViChangeOperation() && count == 0) { |
| break; |
| } |
| while (buf.cursor() < buf.length() && !isWord(buf.currChar())) { |
| buf.move(1); |
| } |
| } |
| return true; |
| } |
| |
| protected boolean viForwardWord() { |
| if (count < 0) { |
| return callNeg(this::backwardWord); |
| } |
| while (count-- > 0) { |
| if (isViAlphaNum(buf.currChar())) { |
| while (buf.cursor() < buf.length() && isViAlphaNum(buf.currChar())) { |
| buf.move(1); |
| } |
| } else { |
| while (buf.cursor() < buf.length() |
| && !isViAlphaNum(buf.currChar()) |
| && !isWhitespace(buf.currChar())) { |
| buf.move(1); |
| } |
| } |
| if (isInViChangeOperation() && count == 0) { |
| return true; |
| } |
| int nl = buf.currChar() == '\n' ? 1 : 0; |
| while (buf.cursor() < buf.length() |
| && nl < 2 |
| && isWhitespace(buf.currChar())) { |
| buf.move(1); |
| nl += buf.currChar() == '\n' ? 1 : 0; |
| } |
| } |
| return true; |
| } |
| |
| protected boolean viForwardBlankWord() { |
| if (count < 0) { |
| return callNeg(this::viBackwardBlankWord); |
| } |
| while (count-- > 0) { |
| while (buf.cursor() < buf.length() && !isWhitespace(buf.currChar())) { |
| buf.move(1); |
| } |
| if (isInViChangeOperation() && count == 0) { |
| return true; |
| } |
| int nl = buf.currChar() == '\n' ? 1 : 0; |
| while (buf.cursor() < buf.length() |
| && nl < 2 |
| && isWhitespace(buf.currChar())) { |
| buf.move(1); |
| nl += buf.currChar() == '\n' ? 1 : 0; |
| } |
| } |
| return true; |
| } |
| |
| protected boolean emacsForwardWord() { |
| if (count < 0) { |
| return callNeg(this::emacsBackwardWord); |
| } |
| while (count-- > 0) { |
| while (buf.cursor() < buf.length() && !isWord(buf.currChar())) { |
| buf.move(1); |
| } |
| if (isInViChangeOperation() && count == 0) { |
| return true; |
| } |
| while (buf.cursor() < buf.length() && isWord(buf.currChar())) { |
| buf.move(1); |
| } |
| } |
| return true; |
| } |
| |
| protected boolean viForwardBlankWordEnd() { |
| if (count < 0) { |
| return false; |
| } |
| while (count-- > 0) { |
| while (buf.cursor() < buf.length()) { |
| buf.move(1); |
| if (!isWhitespace(buf.currChar())) { |
| break; |
| } |
| } |
| while (buf.cursor() < buf.length()) { |
| buf.move(1); |
| if (isWhitespace(buf.currChar())) { |
| break; |
| } |
| } |
| } |
| return true; |
| } |
| |
| protected boolean viForwardWordEnd() { |
| if (count < 0) { |
| return callNeg(this::backwardWord); |
| } |
| while (count-- > 0) { |
| while (buf.cursor() < buf.length()) { |
| if (!isWhitespace(buf.nextChar())) { |
| break; |
| } |
| buf.move(1); |
| } |
| if (buf.cursor() < buf.length()) { |
| if (isViAlphaNum(buf.nextChar())) { |
| buf.move(1); |
| while (buf.cursor() < buf.length() && isViAlphaNum(buf.nextChar())) { |
| buf.move(1); |
| } |
| } else { |
| buf.move(1); |
| while (buf.cursor() < buf.length() && !isViAlphaNum(buf.nextChar()) && !isWhitespace(buf.nextChar())) { |
| buf.move(1); |
| } |
| } |
| } |
| } |
| if (buf.cursor() < buf.length() && isInViMoveOperation()) { |
| buf.move(1); |
| } |
| return true; |
| } |
| |
| protected boolean backwardWord() { |
| if (count < 0) { |
| return callNeg(this::forwardWord); |
| } |
| while (count-- > 0) { |
| while (buf.cursor() > 0 && !isWord(buf.atChar(buf.cursor() - 1))) { |
| buf.move(-1); |
| } |
| while (buf.cursor() > 0 && isWord(buf.atChar(buf.cursor() - 1))) { |
| buf.move(-1); |
| } |
| } |
| return true; |
| } |
| |
| protected boolean viBackwardWord() { |
| if (count < 0) { |
| return callNeg(this::backwardWord); |
| } |
| while (count-- > 0) { |
| int nl = 0; |
| while (buf.cursor() > 0) { |
| buf.move(-1); |
| if (!isWhitespace(buf.currChar())) { |
| break; |
| } |
| nl += buf.currChar() == '\n' ? 1 : 0; |
| if (nl == 2) { |
| buf.move(1); |
| break; |
| } |
| } |
| if (buf.cursor() > 0) { |
| if (isViAlphaNum(buf.currChar())) { |
| while (buf.cursor() > 0) { |
| if (!isViAlphaNum(buf.prevChar())) { |
| break; |
| } |
| buf.move(-1); |
| } |
| } else { |
| while (buf.cursor() > 0) { |
| if (isViAlphaNum(buf.prevChar()) || isWhitespace(buf.prevChar())) { |
| break; |
| } |
| buf.move(-1); |
| } |
| } |
| } |
| } |
| return true; |
| } |
| |
| protected boolean viBackwardBlankWord() { |
| if (count < 0) { |
| return callNeg(this::viForwardBlankWord); |
| } |
| while (count-- > 0) { |
| while (buf.cursor() > 0) { |
| buf.move(-1); |
| if (!isWhitespace(buf.currChar())) { |
| break; |
| } |
| } |
| while (buf.cursor() > 0) { |
| buf.move(-1); |
| if (isWhitespace(buf.currChar())) { |
| break; |
| } |
| } |
| } |
| return true; |
| } |
| |
| protected boolean viBackwardWordEnd() { |
| if (count < 0) { |
| return callNeg(this::viForwardWordEnd); |
| } |
| while (count-- > 0 && buf.cursor() > 1) { |
| int start; |
| if (isViAlphaNum(buf.currChar())) { |
| start = 1; |
| } else if (!isWhitespace(buf.currChar())) { |
| start = 2; |
| } else { |
| start = 0; |
| } |
| while (buf.cursor() > 0) { |
| boolean same = (start != 1) && isWhitespace(buf.currChar()); |
| if (start != 0) { |
| same |= isViAlphaNum(buf.currChar()); |
| } |
| if (same == (start == 2)) { |
| break; |
| } |
| buf.move(-1); |
| } |
| while (buf.cursor() > 0 && isWhitespace(buf.currChar())) { |
| buf.move(-1); |
| } |
| } |
| return true; |
| } |
| |
| protected boolean viBackwardBlankWordEnd() { |
| if (count < 0) { |
| return callNeg(this::viForwardBlankWordEnd); |
| } |
| while (count-- > 0) { |
| while (buf.cursor() > 0 && !isWhitespace(buf.currChar())) { |
| buf.move(-1); |
| } |
| while (buf.cursor() > 0 && isWhitespace(buf.currChar())) { |
| buf.move(-1); |
| } |
| } |
| return true; |
| } |
| |
| protected boolean emacsBackwardWord() { |
| if (count < 0) { |
| return callNeg(this::emacsForwardWord); |
| } |
| while (count-- > 0) { |
| while (buf.cursor() > 0) { |
| buf.move(-1); |
| if (isWord(buf.currChar())) { |
| break; |
| } |
| } |
| while (buf.cursor() > 0) { |
| buf.move(-1); |
| if (!isWord(buf.currChar())) { |
| break; |
| } |
| } |
| } |
| return true; |
| } |
| |
| protected boolean backwardDeleteWord() { |
| if (count < 0) { |
| return callNeg(this::deleteWord); |
| } |
| int cursor = buf.cursor(); |
| while (count-- > 0) { |
| while (cursor > 0 && !isWord(buf.atChar(cursor - 1))) { |
| cursor--; |
| } |
| while (cursor > 0 && isWord(buf.atChar(cursor - 1))) { |
| cursor--; |
| } |
| } |
| buf.backspace(buf.cursor() - cursor); |
| return true; |
| } |
| |
| protected boolean viBackwardKillWord() { |
| if (count < 0) { |
| return false; |
| } |
| int lim = findbol(); |
| int x = buf.cursor(); |
| while (count-- > 0) { |
| while (x > lim && isWhitespace(buf.atChar(x - 1))) { |
| x--; |
| } |
| if (x > lim) { |
| if (isViAlphaNum(buf.atChar(x - 1))) { |
| while (x > lim && isViAlphaNum(buf.atChar(x - 1))) { |
| x--; |
| } |
| } else { |
| while (x > lim && !isViAlphaNum(buf.atChar(x - 1)) && !isWhitespace(buf.atChar(x - 1))) { |
| x--; |
| } |
| } |
| } |
| } |
| killRing.addBackwards(buf.substring(x, buf.cursor())); |
| buf.backspace(buf.cursor() - x); |
| return true; |
| } |
| |
| protected boolean backwardKillWord() { |
| if (count < 0) { |
| return callNeg(this::killWord); |
| } |
| int x = buf.cursor(); |
| while (count-- > 0) { |
| while (x > 0 && !isWord(buf.atChar(x - 1))) { |
| x--; |
| } |
| while (x > 0 && isWord(buf.atChar(x - 1))) { |
| x--; |
| } |
| } |
| killRing.addBackwards(buf.substring(x, buf.cursor())); |
| buf.backspace(buf.cursor() - x); |
| return true; |
| } |
| |
| protected boolean copyPrevWord() { |
| if (count <= 0) { |
| return false; |
| } |
| int t1, t0 = buf.cursor(); |
| while (true) { |
| t1 = t0; |
| while (t0 > 0 && !isWord(buf.atChar(t0 - 1))) { |
| t0--; |
| } |
| while (t0 > 0 && isWord(buf.atChar(t0 - 1))) { |
| t0--; |
| } |
| if (--count == 0) { |
| break; |
| } |
| if (t0 == 0) { |
| return false; |
| } |
| } |
| buf.write(buf.substring(t0, t1)); |
| return true; |
| } |
| |
| protected boolean upCaseWord() { |
| int count = Math.abs(this.count); |
| int cursor = buf.cursor(); |
| while (count-- > 0) { |
| while (buf.cursor() < buf.length() && !isWord(buf.currChar())) { |
| buf.move(1); |
| } |
| while (buf.cursor() < buf.length() && isWord(buf.currChar())) { |
| buf.currChar(Character.toUpperCase(buf.currChar())); |
| buf.move(1); |
| } |
| } |
| if (this.count < 0) { |
| buf.cursor(cursor); |
| } |
| return true; |
| } |
| |
| protected boolean downCaseWord() { |
| int count = Math.abs(this.count); |
| int cursor = buf.cursor(); |
| while (count-- > 0) { |
| while (buf.cursor() < buf.length() && !isWord(buf.currChar())) { |
| buf.move(1); |
| } |
| while (buf.cursor() < buf.length() && isWord(buf.currChar())) { |
| buf.currChar(Character.toLowerCase(buf.currChar())); |
| buf.move(1); |
| } |
| } |
| if (this.count < 0) { |
| buf.cursor(cursor); |
| } |
| return true; |
| } |
| |
| protected boolean capitalizeWord() { |
| int count = Math.abs(this.count); |
| int cursor = buf.cursor(); |
| while (count-- > 0) { |
| boolean first = true; |
| while (buf.cursor() < buf.length() && !isWord(buf.currChar())) { |
| buf.move(1); |
| } |
| while (buf.cursor() < buf.length() && isWord(buf.currChar()) && !isAlpha(buf.currChar())) { |
| buf.move(1); |
| } |
| while (buf.cursor() < buf.length() && isWord(buf.currChar())) { |
| buf.currChar(first |
| ? Character.toUpperCase(buf.currChar()) |
| : Character.toLowerCase(buf.currChar())); |
| buf.move(1); |
| first = false; |
| } |
| } |
| if (this.count < 0) { |
| buf.cursor(cursor); |
| } |
| return true; |
| } |
| |
| protected boolean deleteWord() { |
| if (count < 0) { |
| return callNeg(this::backwardDeleteWord); |
| } |
| int x = buf.cursor(); |
| while (count-- > 0) { |
| while (x < buf.length() && !isWord(buf.atChar(x))) { |
| x++; |
| } |
| while (x < buf.length() && isWord(buf.atChar(x))) { |
| x++; |
| } |
| } |
| buf.delete(x - buf.cursor()); |
| return true; |
| } |
| |
| protected boolean killWord() { |
| if (count < 0) { |
| return callNeg(this::backwardKillWord); |
| } |
| int x = buf.cursor(); |
| while (count-- > 0) { |
| while (x < buf.length() && !isWord(buf.atChar(x))) { |
| x++; |
| } |
| while (x < buf.length() && isWord(buf.atChar(x))) { |
| x++; |
| } |
| } |
| killRing.add(buf.substring(buf.cursor(), x)); |
| buf.delete(x - buf.cursor()); |
| return true; |
| } |
| |
| protected boolean transposeWords() { |
| int lstart = buf.cursor() - 1; |
| int lend = buf.cursor(); |
| while (buf.atChar(lstart) != 0 && buf.atChar(lstart) != '\n') { |
| lstart--; |
| } |
| lstart++; |
| while (buf.atChar(lend) != 0 && buf.atChar(lend) != '\n') { |
| lend++; |
| } |
| if (lend - lstart < 2) { |
| return false; |
| } |
| int words = 0; |
| boolean inWord = false; |
| if (!isDelimiter(buf.atChar(lstart))) { |
| words++; |
| inWord = true; |
| } |
| for (int i = lstart; i < lend; i++) { |
| if (isDelimiter(buf.atChar(i))) { |
| inWord = false; |
| } else { |
| if (!inWord) { |
| words++; |
| } |
| inWord = true; |
| } |
| } |
| if (words < 2) { |
| return false; |
| } |
| // TODO: use isWord instead of isDelimiter |
| boolean neg = this.count < 0; |
| for (int count = Math.max(this.count, -this.count); count > 0; --count) { |
| int sta1, end1, sta2, end2; |
| // Compute current word boundaries |
| sta1 = buf.cursor(); |
| while (sta1 > lstart && !isDelimiter(buf.atChar(sta1 - 1))) { |
| sta1--; |
| } |
| end1 = sta1; |
| while (end1 < lend && !isDelimiter(buf.atChar(++end1))); |
| if (neg) { |
| end2 = sta1 - 1; |
| while (end2 > lstart && isDelimiter(buf.atChar(end2 - 1))) { |
| end2--; |
| } |
| if (end2 < lstart) { |
| // No word before, use the word after |
| sta2 = end1; |
| while (isDelimiter(buf.atChar(++sta2))); |
| end2 = sta2; |
| while (end2 < lend && !isDelimiter(buf.atChar(++end2))); |
| } else { |
| sta2 = end2; |
| while (sta2 > lstart && !isDelimiter(buf.atChar(sta2 - 1))) { |
| sta2--; |
| } |
| } |
| } else { |
| sta2 = end1; |
| while (sta2 < lend && isDelimiter(buf.atChar(++sta2))); |
| if (sta2 == lend) { |
| // No word after, use the word before |
| end2 = sta1; |
| while (isDelimiter(buf.atChar(end2 - 1))) { |
| end2--; |
| } |
| sta2 = end2; |
| while (sta2 > lstart && !isDelimiter(buf.atChar(sta2 - 1))) { |
| sta2--; |
| } |
| } else { |
| end2 = sta2; |
| while (end2 < lend && !isDelimiter(buf.atChar(++end2))) ; |
| } |
| } |
| if (sta1 < sta2) { |
| String res = buf.substring(0, sta1) + buf.substring(sta2, end2) |
| + buf.substring(end1, sta2) + buf.substring(sta1, end1) |
| + buf.substring(end2); |
| buf.clear(); |
| buf.write(res); |
| buf.cursor(neg ? end1 : end2); |
| } else { |
| String res = buf.substring(0, sta2) + buf.substring(sta1, end1) |
| + buf.substring(end2, sta1) + buf.substring(sta2, end2) |
| + buf.substring(end1); |
| buf.clear(); |
| buf.write(res); |
| buf.cursor(neg ? end2 : end1); |
| } |
| } |
| return true; |
| } |
| |
| private int findbol() { |
| int x = buf.cursor(); |
| while (x > 0 && buf.atChar(x - 1) != '\n') { |
| x--; |
| } |
| return x; |
| } |
| |
| private int findeol() { |
| int x = buf.cursor(); |
| while (x < buf.length() && buf.atChar(x) != '\n') { |
| x++; |
| } |
| return x; |
| } |
| |
| protected boolean insertComment() { |
| return doInsertComment(false); |
| } |
| |
| protected boolean viInsertComment() { |
| return doInsertComment(true); |
| } |
| |
| protected boolean doInsertComment(boolean isViMode) { |
| String comment = getString(COMMENT_BEGIN, DEFAULT_COMMENT_BEGIN); |
| beginningOfLine(); |
| putString(comment); |
| if (isViMode) { |
| setKeyMap(VIINS); |
| } |
| return acceptLine(); |
| } |
| |
| protected boolean viFindNextChar() { |
| if ((findChar = vigetkey()) > 0) { |
| findDir = 1; |
| findTailAdd = 0; |
| return vifindchar(false); |
| } |
| return false; |
| } |
| |
| protected boolean viFindPrevChar() { |
| if ((findChar = vigetkey()) > 0) { |
| findDir = -1; |
| findTailAdd = 0; |
| return vifindchar(false); |
| } |
| return false; |
| } |
| |
| protected boolean viFindNextCharSkip() { |
| if ((findChar = vigetkey()) > 0) { |
| findDir = 1; |
| findTailAdd = -1; |
| return vifindchar(false); |
| } |
| return false; |
| } |
| |
| protected boolean viFindPrevCharSkip() { |
| if ((findChar = vigetkey()) > 0) { |
| findDir = -1; |
| findTailAdd = 1; |
| return vifindchar(false); |
| } |
| return false; |
| } |
| |
| protected boolean viRepeatFind() { |
| return vifindchar(true); |
| } |
| |
| protected boolean viRevRepeatFind() { |
| if (count < 0) { |
| return callNeg(() -> vifindchar(true)); |
| } |
| findTailAdd = -findTailAdd; |
| findDir = -findDir; |
| boolean ret = vifindchar(true); |
| findTailAdd = -findTailAdd; |
| findDir = -findDir; |
| return ret; |
| } |
| |
| private int vigetkey() { |
| int ch = readCharacter(); |
| KeyMap<Binding> km = keyMaps.get(MAIN); |
| if (km != null) { |
| Binding b = km.getBound(new String(Character.toChars(ch))); |
| if (b instanceof Reference) { |
| String func = ((Reference) b).name(); |
| if (SEND_BREAK.equals(func)) { |
| return -1; |
| } |
| } |
| } |
| return ch; |
| } |
| |
| private boolean vifindchar(boolean repeat) { |
| if (findDir == 0) { |
| return false; |
| } |
| if (count < 0) { |
| return callNeg(this::viRevRepeatFind); |
| } |
| if (repeat && findTailAdd != 0) { |
| if (findDir > 0) { |
| if (buf.cursor() < buf.length() && buf.nextChar() == findChar) { |
| buf.move(1); |
| } |
| } else { |
| if (buf.cursor() > 0 && buf.prevChar() == findChar) { |
| buf.move(-1); |
| } |
| } |
| } |
| int cursor = buf.cursor(); |
| while (count-- > 0) { |
| do { |
| buf.move(findDir); |
| } while (buf.cursor() > 0 && buf.cursor() < buf.length() |
| && buf.currChar() != findChar |
| && buf.currChar() != '\n'); |
| if (buf.cursor() <= 0 || buf.cursor() >= buf.length() |
| || buf.currChar() == '\n') { |
| buf.cursor(cursor); |
| return false; |
| } |
| } |
| if (findTailAdd != 0) { |
| buf.move(findTailAdd); |
| } |
| if (findDir == 1 && isInViMoveOperation()) { |
| buf.move(1); |
| } |
| return true; |
| } |
| |
| private boolean callNeg(Widget widget) { |
| this.count = -this.count; |
| boolean ret = widget.apply(); |
| this.count = -this.count; |
| return ret; |
| } |
| |
| /** |
| * Implements vi search ("/" or "?"). |
| * |
| * @return <code>true</code> if the search was successful |
| */ |
| protected boolean viHistorySearchForward() { |
| searchDir = 1; |
| searchIndex = 0; |
| return getViSearchString() && viRepeatSearch(); |
| } |
| |
| protected boolean viHistorySearchBackward() { |
| searchDir = -1; |
| searchIndex = history.size() - 1; |
| return getViSearchString() && viRepeatSearch(); |
| } |
| |
| protected boolean viRepeatSearch() { |
| if (searchDir == 0) { |
| return false; |
| } |
| int si = searchDir < 0 |
| ? searchBackwards(searchString, searchIndex, false) |
| : searchForwards(searchString, searchIndex, false); |
| if (si == -1 || si == history.index()) { |
| return false; |
| } |
| searchIndex = si; |
| |
| /* |
| * Show the match. |
| */ |
| buf.clear(); |
| history.moveTo(searchIndex); |
| buf.write(history.get(searchIndex)); |
| if (VICMD.equals(keyMap)) { |
| buf.move(-1); |
| } |
| return true; |
| } |
| |
| protected boolean viRevRepeatSearch() { |
| boolean ret; |
| searchDir = -searchDir; |
| ret = viRepeatSearch(); |
| searchDir = -searchDir; |
| return ret; |
| } |
| |
| private boolean getViSearchString() { |
| if (searchDir == 0) { |
| return false; |
| } |
| String searchPrompt = searchDir < 0 ? "?" : "/"; |
| Buffer searchBuffer = new BufferImpl(); |
| |
| KeyMap<Binding> keyMap = keyMaps.get(MAIN); |
| if (keyMap == null) { |
| keyMap = keyMaps.get(SAFE); |
| } |
| while (true) { |
| post = () -> new AttributedString(searchPrompt + searchBuffer.toString() + "_"); |
| redisplay(); |
| Binding b = doReadBinding(keyMap, null); |
| if (b instanceof Reference) { |
| String func = ((Reference) b).name(); |
| switch (func) { |
| case SEND_BREAK: |
| post = null; |
| return false; |
| case ACCEPT_LINE: |
| case VI_CMD_MODE: |
| searchString = searchBuffer.toString(); |
| post = null; |
| return true; |
| case MAGIC_SPACE: |
| searchBuffer.write(' '); |
| break; |
| case REDISPLAY: |
| redisplay(); |
| break; |
| case CLEAR_SCREEN: |
| clearScreen(); |
| break; |
| case SELF_INSERT: |
| searchBuffer.write(getLastBinding()); |
| break; |
| case SELF_INSERT_UNMETA: |
| if (getLastBinding().charAt(0) == '\u001b') { |
| String s = getLastBinding().substring(1); |
| if ("\r".equals(s)) { |
| s = "\n"; |
| } |
| searchBuffer.write(s); |
| } |
| break; |
| case BACKWARD_DELETE_CHAR: |
| case VI_BACKWARD_DELETE_CHAR: |
| if (searchBuffer.length() > 0) { |
| searchBuffer.backspace(); |
| } |
| break; |
| case BACKWARD_KILL_WORD: |
| case VI_BACKWARD_KILL_WORD: |
| if (searchBuffer.length() > 0 && !isWhitespace(searchBuffer.prevChar())) { |
| searchBuffer.backspace(); |
| } |
| if (searchBuffer.length() > 0 && isWhitespace(searchBuffer.prevChar())) { |
| searchBuffer.backspace(); |
| } |
| break; |
| case QUOTED_INSERT: |
| case VI_QUOTED_INSERT: |
| int c = readCharacter(); |
| if (c >= 0) { |
| searchBuffer.write(c); |
| } else { |
| beep(); |
| } |
| break; |
| default: |
| beep(); |
| break; |
| } |
| } |
| } |
| } |
| |
| protected boolean insertCloseCurly() { |
| return insertClose("}"); |
| } |
| |
| protected boolean insertCloseParen() { |
| return insertClose(")"); |
| } |
| |
| protected boolean insertCloseSquare() { |
| return insertClose("]"); |
| } |
| |
| protected boolean insertClose(String s) { |
| putString(s); |
| |
| long blink = getLong(BLINK_MATCHING_PAREN, DEFAULT_BLINK_MATCHING_PAREN); |
| if (blink <= 0) { |
| removeIndentation(); |
| return true; |
| } |
| |
| int closePosition = buf.cursor(); |
| |
| buf.move(-1); |
| doViMatchBracket(); |
| redisplay(); |
| |
| peekCharacter(blink); |
| int blinkPosition = buf.cursor(); |
| buf.cursor(closePosition); |
| |
| if (blinkPosition != closePosition - 1) { |
| removeIndentation(); |
| } |
| return true; |
| } |
| |
| private void removeIndentation() { |
| int indent = getInt(INDENTATION, DEFAULT_INDENTATION); |
| if (indent > 0) { |
| buf.move(-1); |
| for (int i = 0; i < indent; i++) { |
| buf.move(-1); |
| if (buf.currChar() == ' ') { |
| buf.delete(); |
| } else { |
| buf.move(1); |
| break; |
| } |
| } |
| buf.move(1); |
| } |
| } |
| |
| protected boolean viMatchBracket() { |
| return doViMatchBracket(); |
| } |
| |
| protected boolean undefinedKey() { |
| return false; |
| } |
| |
| /** |
| * Implements vi style bracket matching ("%" command). The matching |
| * bracket for the current bracket type that you are sitting on is matched. |
| * |
| * @return true if it worked, false if the cursor was not on a bracket |
| * character or if there was no matching bracket. |
| */ |
| protected boolean doViMatchBracket() { |
| int pos = buf.cursor(); |
| |
| if (pos == buf.length()) { |
| return false; |
| } |
| |
| int type = getBracketType(buf.atChar(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.length()) { |
| return false; |
| } |
| |
| int curType = getBracketType(buf.atChar(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 && isInViMoveOperation()) |
| ++pos; |
| |
| buf.cursor(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. |
| */ |
| protected int getBracketType (int ch) { |
| switch (ch) { |
| case '[': return 1; |
| case ']': return -1; |
| case '{': return 2; |
| case '}': return -2; |
| case '(': return 3; |
| case ')': return -3; |
| default: |
| return 0; |
| } |
| } |
| |
| /** |
| * Performs character transpose. The character prior to the cursor and the |
| * character under the cursor are swapped and the cursor is advanced one. |
| * Do not cross line breaks. |
| * @return true |
| */ |
| protected boolean transposeChars() { |
| int lstart = buf.cursor() - 1; |
| int lend = buf.cursor(); |
| while (buf.atChar(lstart) != 0 && buf.atChar(lstart) != '\n') { |
| lstart--; |
| } |
| lstart++; |
| while (buf.atChar(lend) != 0 && buf.atChar(lend) != '\n') { |
| lend++; |
| } |
| if (lend - lstart < 2) { |
| return false; |
| } |
| boolean neg = this.count < 0; |
| for (int count = Math.max(this.count, -this.count); count > 0; --count) { |
| while (buf.cursor() <= lstart) { |
| buf.move(1); |
| } |
| while (buf.cursor() >= lend) { |
| buf.move(-1); |
| } |
| int c = buf.currChar(); |
| buf.currChar(buf.prevChar()); |
| buf.move(-1); |
| buf.currChar(c); |
| buf.move(neg ? 0 : 2); |
| } |
| return true; |
| } |
| |
| protected boolean undo() { |
| isUndo = true; |
| if (undo.canUndo()) { |
| undo.undo(); |
| return true; |
| } |
| return false; |
| } |
| |
| protected boolean redo() { |
| isUndo = true; |
| if (undo.canRedo()) { |
| undo.redo(); |
| return true; |
| } |
| return false; |
| } |
| |
| protected boolean sendBreak() { |
| if (searchTerm == null) { |
| buf.clear(); |
| println(); |
| redrawLine(); |
| // state = State.INTERRUPT; |
| return false; |
| } |
| return true; |
| } |
| |
| protected boolean backwardChar() { |
| return buf.move(-count) != 0; |
| } |
| |
| protected boolean forwardChar() { |
| return buf.move(count) != 0; |
| } |
| |
| protected boolean viDigitOrBeginningOfLine() { |
| if (repeatCount > 0) { |
| return digitArgument(); |
| } else { |
| return beginningOfLine(); |
| } |
| } |
| |
| protected boolean universalArgument() { |
| mult *= universal; |
| isArgDigit = true; |
| return true; |
| } |
| |
| protected boolean argumentBase() { |
| if (repeatCount > 0 && repeatCount < 32) { |
| universal = repeatCount; |
| isArgDigit = true; |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| protected boolean negArgument() { |
| mult *= -1; |
| isArgDigit = true; |
| return true; |
| } |
| |
| protected boolean digitArgument() { |
| String s = getLastBinding(); |
| repeatCount = (repeatCount * 10) + s.charAt(s.length() - 1) - '0'; |
| isArgDigit = true; |
| return true; |
| } |
| |
| protected boolean viDelete() { |
| int cursorStart = buf.cursor(); |
| Binding o = readBinding(getKeys()); |
| if (o instanceof Reference) { |
| // TODO: be smarter on how to get the vi range |
| String op = viDeleteChangeYankToRemap(((Reference) o).name()); |
| // 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 (VI_DELETE.equals(op)) { |
| killWholeLine(); |
| } else { |
| viMoveMode = ViMoveMode.DELETE; |
| Widget widget = widgets.get(op); |
| if (widget != null && !widget.apply()) { |
| viMoveMode = ViMoveMode.NORMAL; |
| return false; |
| } |
| viMoveMode = ViMoveMode.NORMAL; |
| } |
| return viDeleteTo(cursorStart, buf.cursor()); |
| } else { |
| pushBackBinding(); |
| return false; |
| } |
| } |
| |
| protected boolean viYankTo() { |
| int cursorStart = buf.cursor(); |
| Binding o = readBinding(getKeys()); |
| if (o instanceof Reference) { |
| // TODO: be smarter on how to get the vi range |
| String op = viDeleteChangeYankToRemap(((Reference) o).name()); |
| // Similar to delete-to, a "yy" yanks the whole line. |
| if (VI_YANK.equals(op)) { |
| yankBuffer = buf.toString(); |
| return true; |
| } else { |
| viMoveMode = ViMoveMode.YANK; |
| Widget widget = widgets.get(op); |
| if (widget != null && !widget.apply()) { |
| return false; |
| } |
| viMoveMode = ViMoveMode.NORMAL; |
| } |
| return viYankTo(cursorStart, buf.cursor()); |
| } else { |
| pushBackBinding(); |
| return false; |
| } |
| } |
| |
| protected boolean viYankWholeLine() { |
| int s, e; |
| int p = buf.cursor(); |
| while (buf.move(-1) == -1 && buf.prevChar() != '\n') ; |
| s = buf.cursor(); |
| for (int i = 0; i < repeatCount; i++) { |
| while (buf.move(1) == 1 && buf.prevChar() != '\n') ; |
| } |
| e = buf.cursor(); |
| yankBuffer = buf.substring(s, e); |
| if (!yankBuffer.endsWith("\n")) { |
| yankBuffer += "\n"; |
| } |
| buf.cursor(p); |
| return true; |
| } |
| |
| protected boolean viChange() { |
| int cursorStart = buf.cursor(); |
| Binding o = readBinding(getKeys()); |
| if (o instanceof Reference) { |
| // TODO: be smarter on how to get the vi range |
| String op = viDeleteChangeYankToRemap(((Reference) o).name()); |
| // change whole line |
| if (VI_CHANGE.equals(op)) { |
| killWholeLine(); |
| } else { |
| viMoveMode = ViMoveMode.CHANGE; |
| Widget widget = widgets.get(op); |
| if (widget != null && !widget.apply()) { |
| viMoveMode = ViMoveMode.NORMAL; |
| return false; |
| } |
| viMoveMode = ViMoveMode.NORMAL; |
| } |
| boolean res = viChange(cursorStart, buf.cursor()); |
| setKeyMap(VIINS); |
| return res; |
| } else { |
| pushBackBinding(); |
| return false; |
| } |
| } |
| |
| /* |
| protected int getViRange(Reference cmd, ViMoveMode mode) { |
| Buffer buffer = buf.copy(); |
| int oldMark = mark; |
| int pos = buf.cursor(); |
| String bind = getLastBinding(); |
| |
| if (visual != 0) { |
| if (buf.length() == 0) { |
| return -1; |
| } |
| pos = mark; |
| v |
| } else { |
| viMoveMode = mode; |
| mark = -1; |
| Binding b = doReadBinding(getKeys(), keyMaps.get(VIOPP)); |
| if (b == null || new Reference(SEND_BREAK).equals(b)) { |
| viMoveMode = ViMoveMode.NORMAL; |
| mark = oldMark; |
| return -1; |
| } |
| if (cmd.equals(b)) { |
| doViLineRange(); |
| } |
| Widget w = getWidget(b); |
| if (w ) |
| if (b instanceof Reference) { |
| |
| } |
| } |
| |
| } |
| */ |
| |
| protected void cleanup() { |
| if (isSet(Option.ERASE_LINE_ON_FINISH)) { |
| Buffer oldBuffer = buf.copy(); |
| AttributedString oldPrompt = prompt; |
| buf.clear(); |
| prompt = new AttributedString(""); |
| doCleanup(false); |
| prompt = oldPrompt; |
| buf.copyFrom(oldBuffer); |
| } else { |
| doCleanup(true); |
| } |
| } |
| |
| protected void doCleanup(boolean nl) { |
| buf.cursor(buf.length()); |
| post = null; |
| if (size.getColumns() > 0 || size.getRows() > 0) { |
| doAutosuggestion = false; |
| redisplay(false); |
| if (nl) { |
| println(); |
| } |
| terminal.puts(Capability.keypad_local); |
| terminal.trackMouse(Terminal.MouseTracking.Off); |
| if (isSet(Option.BRACKETED_PASTE)) |
| terminal.writer().write(BRACKETED_PASTE_OFF); |
| flush(); |
| } |
| history.moveToEnd(); |
| } |
| |
| protected boolean historyIncrementalSearchForward() { |
| return doSearchHistory(false); |
| } |
| |
| protected boolean historyIncrementalSearchBackward() { |
| return doSearchHistory(true); |
| } |
| |
| static class Pair<U,V> { |
| final U u; final V v; |
| public Pair(U u, V v) { |
| this.u = u; |
| this.v = v; |
| } |
| public U getU() { |
| return u; |
| } |
| public V getV() { |
| return v; |
| } |
| } |
| |
| protected boolean doSearchHistory(boolean backward) { |
| if (history.isEmpty()) { |
| return false; |
| } |
| |
| KeyMap<Binding> terminators = new KeyMap<>(); |
| getString(SEARCH_TERMINATORS, DEFAULT_SEARCH_TERMINATORS) |
| .codePoints().forEach(c -> bind(terminators, ACCEPT_LINE, new String(Character.toChars(c)))); |
| |
| Buffer originalBuffer = buf.copy(); |
| searchIndex = -1; |
| searchTerm = new StringBuffer(); |
| searchBackward = backward; |
| searchFailing = false; |
| post = () -> new AttributedString((searchFailing ? "failing" + " " : "") |
| + (searchBackward ? "bck-i-search" : "fwd-i-search") |
| + ": " + searchTerm + "_"); |
| |
| redisplay(); |
| try { |
| while (true) { |
| int prevSearchIndex = searchIndex; |
| Binding operation = readBinding(getKeys(), terminators); |
| String ref = (operation instanceof Reference) ? ((Reference) operation).name() : ""; |
| boolean next = false; |
| switch (ref) { |
| case SEND_BREAK: |
| beep(); |
| buf.copyFrom(originalBuffer); |
| return true; |
| case HISTORY_INCREMENTAL_SEARCH_BACKWARD: |
| searchBackward = true; |
| next = true; |
| break; |
| case HISTORY_INCREMENTAL_SEARCH_FORWARD: |
| searchBackward = false; |
| next = true; |
| break; |
| case BACKWARD_DELETE_CHAR: |
| if (searchTerm.length() > 0) { |
| searchTerm.deleteCharAt(searchTerm.length() - 1); |
| } |
| break; |
| case SELF_INSERT: |
| searchTerm.append(getLastBinding()); |
| break; |
| default: |
| // Set buffer and cursor position to the found string. |
| if (searchIndex != -1) { |
| history.moveTo(searchIndex); |
| } |
| pushBackBinding(); |
| return true; |
| } |
| |
| // print the search status |
| String pattern = doGetSearchPattern(); |
| if (pattern.length() == 0) { |
| buf.copyFrom(originalBuffer); |
| searchFailing = false; |
| } else { |
| boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE_SEARCH); |
| Pattern pat = Pattern.compile(pattern, caseInsensitive ? Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE |
| : Pattern.UNICODE_CASE); |
| Pair<Integer, Integer> pair = null; |
| if (searchBackward) { |
| boolean nextOnly = next; |
| pair = matches(pat, buf.toString(), searchIndex).stream() |
| .filter(p -> nextOnly ? p.v < buf.cursor() : p.v <= buf.cursor()) |
| .max(Comparator.comparing(Pair::getV)) |
| .orElse(null); |
| if (pair == null) { |
| pair = StreamSupport.stream( |
| Spliterators.spliteratorUnknownSize(history.reverseIterator(searchIndex < 0 ? history.last() : searchIndex - 1), Spliterator.ORDERED), false) |
| .flatMap(e -> matches(pat, e.line(), e.index()).stream()) |
| .findFirst() |
| .orElse(null); |
| } |
| } else { |
| boolean nextOnly = next; |
| pair = matches(pat, buf.toString(), searchIndex).stream() |
| .filter(p -> nextOnly ? p.v > buf.cursor() : p.v >= buf.cursor()) |
| .min(Comparator.comparing(Pair::getV)) |
| .orElse(null); |
| if (pair == null) { |
| pair = StreamSupport.stream( |
| Spliterators.spliteratorUnknownSize(history.iterator((searchIndex < 0 ? history.last() : searchIndex) + 1), Spliterator.ORDERED), false) |
| .flatMap(e -> matches(pat, e.line(), e.index()).stream()) |
| .findFirst() |
| .orElse(null); |
| if (pair == null && searchIndex >= 0) { |
| pair = matches(pat, originalBuffer.toString(), -1).stream() |
| .min(Comparator.comparing(Pair::getV)) |
| .orElse(null); |
| } |
| } |
| } |
| if (pair != null) { |
| searchIndex = pair.u; |
| buf.clear(); |
| if (searchIndex >= 0) { |
| buf.write(history.get(searchIndex)); |
| } else { |
| buf.write(originalBuffer.toString()); |
| } |
| buf.cursor(pair.v); |
| searchFailing = false; |
| } else { |
| searchFailing = true; |
| beep(); |
| } |
| } |
| redisplay(); |
| } |
| } catch (IOError e) { |
| // Ignore Ctrl+C interrupts and just exit the loop |
| if (!(e.getCause() instanceof InterruptedException)) { |
| throw e; |
| } |
| return true; |
| } finally { |
| searchTerm = null; |
| searchIndex = -1; |
| post = null; |
| } |
| } |
| |
| private List<Pair<Integer, Integer>> matches(Pattern p, String line, int index) { |
| List<Pair<Integer, Integer>> starts = new ArrayList<>(); |
| Matcher m = p.matcher(line); |
| while (m.find()) { |
| starts.add(new Pair<>(index, m.start())); |
| } |
| return starts; |
| } |
| |
| private String doGetSearchPattern() { |
| StringBuilder sb = new StringBuilder(); |
| boolean inQuote = false; |
| for (int i = 0; i < searchTerm.length(); i++) { |
| char c = searchTerm.charAt(i); |
| if (Character.isLowerCase(c)) { |
| if (inQuote) { |
| sb.append("\\E"); |
| inQuote = false; |
| } |
| sb.append("[").append(Character.toLowerCase(c)).append(Character.toUpperCase(c)).append("]"); |
| } else { |
| if (!inQuote) { |
| sb.append("\\Q"); |
| inQuote = true; |
| } |
| sb.append(c); |
| } |
| } |
| if (inQuote) { |
| sb.append("\\E"); |
| } |
| return sb.toString(); |
| } |
| |
| private void pushBackBinding() { |
| pushBackBinding(false); |
| } |
| |
| private void pushBackBinding(boolean skip) { |
| String s = getLastBinding(); |
| if (s != null) { |
| bindingReader.runMacro(s); |
| skipRedisplay = skip; |
| } |
| } |
| |
| protected boolean historySearchForward() { |
| if (historyBuffer == null || buf.length() == 0 |
| || !buf.toString().equals(history.current())) { |
| historyBuffer = buf.copy(); |
| searchBuffer = getFirstWord(); |
| } |
| int index = history.index() + 1; |
| |
| if (index < history.last() + 1) { |
| int searchIndex = searchForwards(searchBuffer.toString(), index, true); |
| if (searchIndex == -1) { |
| history.moveToEnd(); |
| if (!buf.toString().equals(historyBuffer.toString())) { |
| setBuffer(historyBuffer.toString()); |
| historyBuffer = null; |
| } else { |
| return false; |
| } |
| } else { |
| // Maintain cursor position while searching. |
| if (history.moveTo(searchIndex)) { |
| setBuffer(history.current()); |
| } else { |
| history.moveToEnd(); |
| setBuffer(historyBuffer.toString()); |
| return false; |
| } |
| } |
| } else { |
| history.moveToEnd(); |
| if (!buf.toString().equals(historyBuffer.toString())) { |
| setBuffer(historyBuffer.toString()); |
| historyBuffer = null; |
| } else { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| private CharSequence getFirstWord() { |
| String s = buf.toString(); |
| int i = 0; |
| while (i < s.length() && !Character.isWhitespace(s.charAt(i))) { |
| i++; |
| } |
| return s.substring(0, i); |
| } |
| |
| protected boolean historySearchBackward() { |
| if (historyBuffer == null || buf.length() == 0 |
| || !buf.toString().equals(history.current())) { |
| historyBuffer = buf.copy(); |
| searchBuffer = getFirstWord(); |
| } |
| int searchIndex = searchBackwards(searchBuffer.toString(), history.index(), true); |
| |
| if (searchIndex == -1) { |
| return false; |
| } else { |
| // Maintain cursor position while searching. |
| if (history.moveTo(searchIndex)) { |
| setBuffer(history.current()); |
| } else { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // |
| // 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(), false); |
| } |
| |
| public int searchBackwards(String searchTerm, int startIndex, boolean startsWith) { |
| boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE_SEARCH); |
| if (caseInsensitive) { |
| searchTerm = searchTerm.toLowerCase(); |
| } |
| ListIterator<History.Entry> it = history.iterator(startIndex); |
| while (it.hasPrevious()) { |
| History.Entry e = it.previous(); |
| String line = e.line(); |
| if (caseInsensitive) { |
| line = line.toLowerCase(); |
| } |
| int idx = line.indexOf(searchTerm); |
| if ((startsWith && idx == 0) || (!startsWith && idx >= 0)) { |
| return e.index(); |
| } |
| } |
| return -1; |
| } |
| |
| public int searchForwards(String searchTerm, int startIndex, boolean startsWith) { |
| boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE_SEARCH); |
| if (caseInsensitive) { |
| searchTerm = searchTerm.toLowerCase(); |
| } |
| if (startIndex > history.last()) { |
| startIndex = history.last(); |
| } |
| ListIterator<History.Entry> it = history.iterator(startIndex); |
| if (searchIndex != -1 && it.hasNext()) { |
| it.next(); |
| } |
| while (it.hasNext()) { |
| History.Entry e = it.next(); |
| String line = e.line(); |
| if (caseInsensitive) { |
| line = line.toLowerCase(); |
| } |
| int idx = line.indexOf(searchTerm); |
| if ((startsWith && idx == 0) || (!startsWith && idx >= 0)) { |
| 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()); |
| } |
| |
| protected boolean quit() { |
| getBuffer().clear(); |
| 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; |
| int curPos = 0; |
| if (!isSet(Option.DISABLE_EVENT_EXPANSION)) { |
| try { |
| String str = buf.toString(); |
| String exp = expander.expandHistory(history, str); |
| if (!exp.equals(str)) { |
| buf.clear(); |
| buf.write(exp); |
| if (isSet(Option.HISTORY_VERIFY)) { |
| return true; |
| } |
| } |
| } catch (IllegalArgumentException e) { |
| // Ignore |
| } |
| } |
| try { |
| curPos = buf.cursor(); |
| parsedLine = parser.parse(buf.toString(), buf.cursor(), ParseContext.ACCEPT_LINE); |
| } catch (EOFError e) { |
| StringBuilder sb = new StringBuilder("\n"); |
| indention(e.getOpenBrackets(), sb); |
| int curMove = sb.length(); |
| if (isSet(Option.INSERT_BRACKET) && e.getOpenBrackets() > 1 && e.getNextClosingBracket() != null) { |
| sb.append('\n'); |
| indention(e.getOpenBrackets() - 1, sb); |
| sb.append(e.getNextClosingBracket()); |
| } |
| buf.write(sb.toString()); |
| buf.cursor(curPos + curMove); |
| return true; |
| } catch (SyntaxError e) { |
| // do nothing |
| } |
| callWidget(CALLBACK_FINISH); |
| state = State.DONE; |
| return true; |
| } |
| |
| void indention(int nb, StringBuilder sb) { |
| int indent = getInt(INDENTATION, DEFAULT_INDENTATION)*nb; |
| for (int i = 0; i < indent; i++) { |
| sb.append(' '); |
| } |
| } |
| |
| protected boolean selfInsert() { |
| for (int count = this.count; count > 0; count--) { |
| putString(getLastBinding()); |
| } |
| return true; |
| } |
| |
| protected boolean selfInsertUnmeta() { |
| if (getLastBinding().charAt(0) == '\u001b') { |
| String s = getLastBinding().substring(1); |
| if ("\r".equals(s)) { |
| s = "\n"; |
| } |
| for (int count = this.count; count > 0; count--) { |
| putString(s); |
| } |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| protected boolean overwriteMode() { |
| overTyping = !overTyping; |
| return true; |
| } |
| |
| |
| // |
| // History Control |
| // |
| |
| protected boolean beginningOfBufferOrHistory() { |
| if (findbol() != 0) { |
| buf.cursor(0); |
| return true; |
| } else { |
| return beginningOfHistory(); |
| } |
| } |
| |
| protected boolean beginningOfHistory() { |
| if (history.moveToFirst()) { |
| setBuffer(history.current()); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| protected boolean endOfBufferOrHistory() { |
| if (findeol() != buf.length()) { |
| buf.cursor(buf.length()); |
| return true; |
| } else { |
| return endOfHistory(); |
| } |
| } |
| |
| protected boolean endOfHistory() { |
| if (history.moveToLast()) { |
| setBuffer(history.current()); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| protected boolean beginningOfLineHist() { |
| if (count < 0) { |
| return callNeg(this::endOfLineHist); |
| } |
| while (count-- > 0) { |
| int bol = findbol(); |
| if (bol != buf.cursor()) { |
| buf.cursor(bol); |
| } else { |
| moveHistory(false); |
| buf.cursor(0); |
| } |
| } |
| return true; |
| } |
| |
| protected boolean endOfLineHist() { |
| if (count < 0) { |
| return callNeg(this::beginningOfLineHist); |
| } |
| while (count-- > 0) { |
| int eol = findeol(); |
| if (eol != buf.cursor()) { |
| buf.cursor(eol); |
| } else { |
| moveHistory(true); |
| } |
| } |
| return true; |
| } |
| |
| protected boolean upHistory() { |
| while (count-- > 0) { |
| if (!moveHistory(false)) { |
| return !isSet(Option.HISTORY_BEEP); |
| } |
| } |
| return true; |
| } |
| |
| protected boolean downHistory() { |
| while (count-- > 0) { |
| if (!moveHistory(true)) { |
| return !isSet(Option.HISTORY_BEEP); |
| } |
| } |
| return true; |
| } |
| |
| protected boolean viUpLineOrHistory() { |
| return upLine() |
| || upHistory() && viFirstNonBlank(); |
| } |
| |
| protected boolean viDownLineOrHistory() { |
| return downLine() |
| || downHistory() && viFirstNonBlank(); |
| } |
| |
| protected boolean upLine() { |
| return buf.up(); |
| } |
| |
| protected boolean downLine() { |
| return buf.down(); |
| } |
| |
| protected boolean upLineOrHistory() { |
| return upLine() || upHistory(); |
| } |
| |
| protected boolean upLineOrSearch() { |
| return upLine() || historySearchBackward(); |
| } |
| |
| protected boolean downLineOrHistory() { |
| return downLine() || downHistory(); |
| } |
| |
| protected boolean downLineOrSearch() { |
| return downLine() || historySearchForward(); |
| } |
| |
| protected boolean viCmdMode() { |
| // 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 explicit entry to movement |
| // mode. |
| if (state == State.NORMAL) { |
| buf.move(-1); |
| } |
| return setKeyMap(VICMD); |
| } |
| |
| protected boolean viInsert() { |
| return setKeyMap(VIINS); |
| } |
| |
| protected boolean viAddNext() { |
| buf.move(1); |
| return setKeyMap(VIINS); |
| } |
| |
| protected boolean viAddEol() { |
| return endOfLine() && setKeyMap(VIINS); |
| } |
| |
| protected boolean emacsEditingMode() { |
| return setKeyMap(EMACS); |
| } |
| |
| protected boolean viChangeWholeLine() { |
| return viFirstNonBlank() && viChangeEol(); |
| } |
| |
| protected boolean viChangeEol() { |
| return viChange(buf.cursor(), buf.length()) |
| && setKeyMap(VIINS); |
| } |
| |
| protected boolean viKillEol() { |
| int eol = findeol(); |
| if (buf.cursor() == eol) { |
| return false; |
| } |
| killRing.add(buf.substring(buf.cursor(), eol)); |
| buf.delete(eol - buf.cursor()); |
| return true; |
| } |
| |
| protected boolean quotedInsert() { |
| int c = readCharacter(); |
| while (count-- > 0) { |
| putString(new String(Character.toChars(c))); |
| } |
| return true; |
| } |
| |
| protected boolean viJoin() { |
| if (buf.down()) { |
| while (buf.move(-1) == -1 && buf.prevChar() != '\n') ; |
| buf.backspace(); |
| buf.write(' '); |
| buf.move(-1); |
| return true; |
| } |
| return false; |
| } |
| |
| protected boolean viKillWholeLine() { |
| return killWholeLine() && setKeyMap(VIINS); |
| } |
| |
| protected boolean viInsertBol() { |
| return beginningOfLine() && setKeyMap(VIINS); |
| } |
| |
| protected boolean backwardDeleteChar() { |
| if (count < 0) { |
| return callNeg(this::deleteChar); |
| } |
| if (buf.cursor() == 0) { |
| return false; |
| } |
| buf.backspace(count); |
| return true; |
| } |
| |
| protected boolean viFirstNonBlank() { |
| beginningOfLine(); |
| while (buf.cursor() < buf.length() && isWhitespace(buf.currChar())) { |
| buf.move(1); |
| } |
| return true; |
| } |
| |
| protected boolean viBeginningOfLine() { |
| buf.cursor(findbol()); |
| return true; |
| } |
| |
| protected boolean viEndOfLine() { |
| if (count < 0) { |
| return false; |
| } |
| while (count-- > 0) { |
| buf.cursor(findeol() + 1); |
| } |
| buf.move(-1); |
| return true; |
| } |
| |
| protected boolean beginningOfLine() { |
| while (count-- > 0) { |
| while (buf.move(-1) == -1 && buf.prevChar() != '\n') ; |
| } |
| return true; |
| } |
| |
| protected boolean endOfLine() { |
| while (count-- > 0) { |
| while (buf.move(1) == 1 && buf.currChar() != '\n') ; |
| } |
| return true; |
| } |
| |
| protected boolean deleteChar() { |
| if (count < 0) { |
| return callNeg(this::backwardDeleteChar); |
| } |
| if (buf.cursor() == buf.length()) { |
| return false; |
| } |
| buf.delete(count); |
| return true; |
| } |
| |
| /** |
| * Deletes the previous character from the cursor position |
| * @return <code>true</code> if it succeeded, <code>false</code> otherwise |
| */ |
| protected boolean viBackwardDeleteChar() { |
| for (int i = 0; i < count; i++) { |
| if (!buf.backspace()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Deletes the character you are sitting on and sucks the rest of |
| * the line in from the right. |
| * @return <code>true</code> if it succeeded, <code>false</code> otherwise |
| */ |
| protected boolean viDeleteChar() { |
| for (int i = 0; i < count; i++) { |
| if (!buf.delete()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * 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. |
| * @return <code>true</code> if it succeeded, <code>false</code> otherwise |
| */ |
| protected boolean viSwapCase() { |
| for (int i = 0; i < count; i++) { |
| if (buf.cursor() < buf.length()) { |
| int ch = buf.atChar(buf.cursor()); |
| ch = switchCase(ch); |
| buf.currChar(ch); |
| buf.move(1); |
| } else { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Implements the vi change character command (in move-mode "r" |
| * followed by the character to change to). |
| * @return <code>true</code> if it succeeded, <code>false</code> otherwise |
| */ |
| protected boolean viReplaceChars() { |
| int c = readCharacter(); |
| // EOF, ESC, or CTRL-C aborts. |
| if (c < 0 || c == '\033' || c == '\003') { |
| return true; |
| } |
| |
| for (int i = 0; i < count; i++) { |
| if (buf.currChar((char) c)) { |
| if (i < count - 1) { |
| buf.move(1); |
| } |
| } else { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| protected boolean viChange(int startPos, int endPos) { |
| return doViDeleteOrChange(startPos, endPos, true); |
| } |
| |
| protected boolean viDeleteTo(int startPos, int endPos) { |
| return doViDeleteOrChange(startPos, endPos, false); |
| } |
| |
| /** |
| * 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 <code>true</code> if it succeeded, <code>false</code> otherwise |
| */ |
| protected boolean doViDeleteOrChange(int startPos, int endPos, boolean isChange) { |
| if (startPos == endPos) { |
| return true; |
| } |
| |
| if (endPos < startPos) { |
| int tmp = endPos; |
| endPos = startPos; |
| startPos = tmp; |
| } |
| |
| buf.cursor(startPos); |
| buf.delete(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()) { |
| buf.move(-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 example "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 <code>true</code> if the yank succeeded |
| */ |
| protected boolean viYankTo(int startPos, int endPos) { |
| int cursorPos = startPos; |
| |
| if (endPos < startPos) { |
| int tmp = endPos; |
| endPos = startPos; |
| startPos = tmp; |
| } |
| |
| if (startPos == endPos) { |
| yankBuffer = ""; |
| return true; |
| } |
| |
| yankBuffer = buf.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. |
| */ |
| buf.cursor(cursorPos); |
| return true; |
| } |
| |
| protected boolean viOpenLineAbove() { |
| while (buf.move(-1) == -1 && buf.prevChar() != '\n') ; |
| buf.write('\n'); |
| buf.move(-1); |
| return setKeyMap(VIINS); |
| } |
| |
| protected boolean viOpenLineBelow() { |
| while (buf.move(1) == 1 && buf.currChar() != '\n') ; |
| buf.write('\n'); |
| return setKeyMap(VIINS); |
| } |
| |
| /** |
| * Pasts the yank buffer to the right of the current cursor position |
| * and moves the cursor to the end of the pasted region. |
| * @return <code>true</code> |
| */ |
| protected boolean viPutAfter() { |
| if (yankBuffer.indexOf('\n') >= 0) { |
| while (buf.move(1) == 1 && buf.currChar() != '\n'); |
| buf.move(1); |
| putString(yankBuffer); |
| buf.move(- yankBuffer.length()); |
| } else if (yankBuffer.length () != 0) { |
| if (buf.cursor() < buf.length()) { |
| buf.move(1); |
| } |
| for (int i = 0; i < count; i++) { |
| putString(yankBuffer); |
| } |
| buf.move(-1); |
| } |
| return true; |
| } |
| |
| protected boolean viPutBefore() { |
| if (yankBuffer.indexOf('\n') >= 0) { |
| while (buf.move(-1) == -1 && buf.prevChar() != '\n'); |
| putString(yankBuffer); |
| buf.move(- yankBuffer.length()); |
| } else if (yankBuffer.length () != 0) { |
| if (buf.cursor() > 0) { |
| buf.move(-1); |
| } |
| for (int i = 0; i < count; i++) { |
| putString(yankBuffer); |
| } |
| buf.move(-1); |
| } |
| return true; |
| } |
| |
| protected boolean doLowercaseVersion() { |
| bindingReader.runMacro(getLastBinding().toLowerCase()); |
| return true; |
| } |
| |
| protected boolean setMarkCommand() { |
| if (count < 0) { |
| regionActive = RegionType.NONE; |
| return true; |
| } |
| regionMark = buf.cursor(); |
| regionActive = RegionType.CHAR; |
| return true; |
| } |
| |
| protected boolean exchangePointAndMark() { |
| if (count == 0) { |
| regionActive = RegionType.CHAR; |
| return true; |
| } |
| int x = regionMark; |
| regionMark = buf.cursor(); |
| buf.cursor(x); |
| if (buf.cursor() > buf.length()) { |
| buf.cursor(buf.length()); |
| } |
| if (count > 0) { |
| regionActive = RegionType.CHAR; |
| } |
| return true; |
| } |
| |
| protected boolean visualMode() { |
| if (isInViMoveOperation()) { |
| isArgDigit = true; |
| forceLine = false; |
| forceChar = true; |
| return true; |
| } |
| if (regionActive == RegionType.NONE) { |
| regionMark = buf.cursor(); |
| regionActive = RegionType.CHAR; |
| } else if (regionActive == RegionType.CHAR) { |
| regionActive = RegionType.NONE; |
| } else if (regionActive == RegionType.LINE) { |
| regionActive = RegionType.CHAR; |
| } |
| return true; |
| } |
| |
| protected boolean visualLineMode() { |
| if (isInViMoveOperation()) { |
| isArgDigit = true; |
| forceLine = true; |
| forceChar = false; |
| return true; |
| } |
| if (regionActive == RegionType.NONE) { |
| regionMark = buf.cursor(); |
| regionActive = RegionType.LINE; |
| } else if (regionActive == RegionType.CHAR) { |
| regionActive = RegionType.LINE; |
| } else if (regionActive == RegionType.LINE) { |
| regionActive = RegionType.NONE; |
| } |
| return true; |
| } |
| |
| protected boolean deactivateRegion() { |
| regionActive = RegionType.NONE; |
| return true; |
| } |
| |
| protected boolean whatCursorPosition() { |
| post = () -> { |
| AttributedStringBuilder sb = new AttributedStringBuilder(); |
| if (buf.cursor() < buf.length()) { |
| int c = buf.currChar(); |
| sb.append("Char: "); |
| if (c == ' ') { |
| sb.append("SPC"); |
| } else if (c == '\n') { |
| sb.append("LFD"); |
| } else if (c < 32) { |
| sb.append('^'); |
| sb.append((char) (c + 'A' - 1)); |
| } else if (c == 127) { |
| sb.append("^?"); |
| } else { |
| sb.append((char) c); |
| } |
| sb.append(" ("); |
| sb.append("0").append(Integer.toOctalString(c)).append(" "); |
| sb.append(Integer.toString(c)).append(" "); |
| sb.append("0x").append(Integer.toHexString(c)).append(" "); |
| sb.append(")"); |
| } else { |
| sb.append("EOF"); |
| } |
| sb.append(" "); |
| sb.append("point "); |
| sb.append(Integer.toString(buf.cursor() + 1)); |
| sb.append(" of "); |
| sb.append(Integer.toString(buf.length() + 1)); |
| sb.append(" ("); |
| sb.append(Integer.toString(buf.length() == 0 ? 100 : ((100 * buf.cursor()) / buf.length()))); |
| sb.append("%)"); |
| sb.append(" "); |
| sb.append("column "); |
| sb.append(Integer.toString(buf.cursor() - findbol())); |
| return sb.toAttributedString(); |
| }; |
| return true; |
| } |
| |
| protected boolean editAndExecute() { |
| boolean out = true; |
| File file = null; |
| try { |
| file = File.createTempFile("jline-execute-", null); |
| FileWriter writer = new FileWriter(file); |
| writer.write(buf.toString()); |
| writer.close(); |
| editAndAddInBuffer(file); |
| } catch (Exception e) { |
| e.printStackTrace(terminal.writer()); |
| out = false; |
| } finally { |
| state = State.IGNORE; |
| if (file != null && file.exists()) { |
| file.delete(); |
| } |
| } |
| return out; |
| } |
| |
| protected Map<String, Widget> builtinWidgets() { |
| Map<String, Widget> widgets = new HashMap<>(); |
| 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, EDIT_AND_EXECUTE_COMMAND, this::editAndExecute); |
| 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 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(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) { |
| if (lines.size() > 1) { |
| for (int i = 0; i < lines.size() - 1; i++) { |
| sb.append(lines.get(i)); |
| sb.style(sb.style().inverse()); |
| sb.append("\\n"); |
| sb.style(sb.style().inverseOff()); |
| } |
| } |
| sb.append(lines.get(lines.size() - 1)); |
| } |
| |
| private String matchPreviousCommand(String buffer) { |
| if (buffer.length() == 0) { |
| return ""; |
| } |
| History history = getHistory(); |
| StringBuilder sb = new StringBuilder(); |
| char prev = '0'; |
| for (char c: buffer.toCharArray()) { |
| if ((c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}' || c == '^') && prev != '\\' ) { |
| sb.append('\\'); |
| } |
| sb.append(c); |
| prev = c; |
| } |
| Pattern pattern = Pattern.compile(sb.toString() + ".*", Pattern.DOTALL); |
| Iterator<History.Entry> iter = history.reverseIterator(history.last()); |
| String suggestion = ""; |
| int tot = 0; |
| while (iter.hasNext()) { |
| History.Entry entry = iter.next(); |
| Matcher matcher = pattern.matcher(entry.line()); |
| if (matcher.matches()) { |
| suggestion = entry.line().substring(buffer.length()); |
| break; |
| } else if (tot > 200) { |
| break; |
| } |
| tot++; |
| } |
| return suggestion; |
| } |
| |
| /** |
| * Compute the full string to be displayed with the left, right and secondary prompts |
| * @param secondaryPrompts a list to store the secondary prompts |
| * @return the displayed string including the buffer, left prompts and the help below |
| */ |
| public AttributedString getDisplayedBufferWithPrompts(List<AttributedString> secondaryPrompts) { |
| AttributedString attBuf = getHighlightedBuffer(buf.toString()); |
| |
| AttributedString tNewBuf = insertSecondaryPrompts(attBuf, secondaryPrompts); |
| AttributedStringBuilder full = new AttributedStringBuilder().tabs(TAB_WIDTH); |
| full.append(prompt); |
| full.append(tNewBuf); |
| if (doAutosuggestion) { |
| String lastBinding = getLastBinding() != null ? getLastBinding() : ""; |
| if (autosuggestion == SuggestionType.HISTORY) { |
| AttributedStringBuilder sb = new AttributedStringBuilder(); |
| tailTip = matchPreviousCommand(buf.toString()); |
| sb.styled(AttributedStyle::faint, tailTip); |
| full.append(sb.toAttributedString()); |
| } else if (autosuggestion == SuggestionType.COMPLETER) { |
| if (buf.length() > 0 && buf.length() == buf.cursor() |
| && (!lastBinding.equals("\t") || buf.prevChar() == ' ' || buf.prevChar() == '=')) { |
| clearChoices(); |
| listChoices(true); |
| } else if (!lastBinding.equals("\t")) { |
| clearChoices(); |
| } |
| } else if (autosuggestion == SuggestionType.TAIL_TIP) { |
| if (buf.length() == buf.cursor()) { |
| if (!lastBinding.equals("\t") || buf.prevChar() == ' ') { |
| clearChoices(); |
| } |
| AttributedStringBuilder sb = new AttributedStringBuilder(); |
| if (buf.prevChar() != ' ') { |
| if (!tailTip.startsWith("[")) { |
| int idx = tailTip.indexOf(' '); |
| int idb = buf.toString().lastIndexOf(' '); |
| int idd = buf.toString().lastIndexOf('-'); |
| if (idx > 0 && ((idb == -1 && idb == idd) || (idb >= 0 && idb > idd))) { |
| tailTip = tailTip.substring(idx); |
| } else if (idb >= 0 && idb < idd) { |
| sb.append(" "); |
| } |
| } else { |
| sb.append(" "); |
| } |
| } |
| sb.styled(AttributedStyle::faint, tailTip); |
| full.append(sb.toAttributedString()); |
| } |
| } |
| } |
| if (post != null) { |
| full.append("\n"); |
| full.append(post.get()); |
| } |
| doAutosuggestion = true; |
| return full.toAttributedString(); |
| } |
| |
| private AttributedString getHighlightedBuffer(String buffer) { |
| if (maskingCallback != null) { |
| buffer = maskingCallback.display(buffer); |
| } |
| if (highlighter != null && !isSet(Option.DISABLE_HIGHLIGHTER) |
| && buffer.length() < getInt(FEATURES_MAX_BUFFER_SIZE, DEFAULT_FEATURES_MAX_BUFFER_SIZE)) { |
| return highlighter.highlight(this, buffer); |
| } |
| return new AttributedString(buffer); |
| } |
| |
| private AttributedString expandPromptPattern(String pattern, int padToWidth, |
| String message, int line) { |
| ArrayList<AttributedString> parts = new ArrayList<>(); |
| boolean isHidden = false; |
| int padPartIndex = -1; |
| StringBuilder padPartString = null; |
| StringBuilder sb = new StringBuilder(); |
| // Add "%{" to avoid special case for end of string. |
| pattern = pattern + "%{"; |
| int plen = pattern.length(); |
| int padChar = -1; |
| int padPos = -1; |
| int cols = 0; |
| for (int i = 0; i < plen; ) { |
| char ch = pattern.charAt(i++); |
| if (ch == '%' && i < plen) { |
| int count = 0; |
| boolean countSeen = false; |
| 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 == '{'; |
| break decode; |
| case '%': |
| sb.append(ch); |
| break decode; |
| case 'N': |
| sb.append(getInt(LINE_OFFSET, 0) + line); |
| break decode; |
| case 'M': |
| if (message != null) |
| sb.append(message); |
| break decode; |
| case 'P': |
| if (countSeen && count >= 0) |
| padToWidth = count; |
| if (i < plen) { |
| padChar = pattern.charAt(i++); |
| // FIXME check surrogate |
| } |
| padPos = sb.length(); |
| padPartIndex = parts.size(); |
| break decode; |
| case '-': |
| case '0': |
| case '1': |
| case '2': |
| case '3': |
| case '4': |
| case '5': |
| case '6': |
| case '7': |
| case '8': |
| case '9': |
| boolean neg = false; |
| if (ch == '-') { |
| neg = true; |
| ch = pattern.charAt(i++); |
| } |
| countSeen = true; |
| count = 0; |
| while (ch >= '0' && ch <= '9') { |
| count = (count < 0 ? 0 : 10 * count) + (ch - '0'); |
| ch = pattern.charAt(i++); |
| } |
| if (neg) { |
| count = -count; |
| } |
| i--; |
| break; |
| default: |
| break decode; |
| } |
| } |
| } else |
| sb.append(ch); |
| } |
| if (padToWidth > cols) { |
| int padCharCols = WCWidth.wcwidth(padChar); |
| int padCount = (padToWidth - cols) / padCharCols; |
| sb = padPartString; |
| while (--padCount >= 0) |
| sb.insert(padPos, (char) padChar); // FIXME if wide |
| parts.set(padPartIndex, AttributedString.fromAnsi(sb.toString())); |
| } |
| return AttributedString.join(null, parts); |
| } |
| |
| private AttributedString insertSecondaryPrompts(AttributedString str, List<AttributedString> prompts) { |
| return insertSecondaryPrompts(str, prompts, true); |
| } |
| |
| private AttributedString insertSecondaryPrompts(AttributedString strAtt, List<AttributedString> prompts, boolean computePrompts) { |
| Objects.requireNonNull(prompts); |
| List<AttributedString> lines = strAtt.columnSplitLength(Integer.MAX_VALUE); |
| AttributedStringBuilder sb = new AttributedStringBuilder(); |
| String secondaryPromptPattern = getString(SECONDARY_PROMPT_PATTERN, DEFAULT_SECONDARY_PROMPT_PATTERN); |
| boolean needsMessage = secondaryPromptPattern.contains("%M") |
| && strAtt.length() < getInt(FEATURES_MAX_BUFFER_SIZE, DEFAULT_FEATURES_MAX_BUFFER_SIZE); |
| AttributedStringBuilder buf = new AttributedStringBuilder(); |
| int width = 0; |
| List<String> missings = new ArrayList<>(); |
| if (computePrompts && secondaryPromptPattern.contains("%P")) { |
| width = prompt.columnLength(); |
| for (int line = 0; line < lines.size() - 1; line++) { |
| AttributedString prompt; |
| buf.append(lines.get(line)).append("\n"); |
| String missing = ""; |
| if (needsMessage) { |
| try { |
| parser.parse(buf.toString(), buf.length(), ParseContext.SECONDARY_PROMPT); |
| } catch (EOFError e) { |
| missing = e.getMissing(); |
| } catch (SyntaxError e) { |
| // Ignore |
| } |
| } |
| missings.add(missing); |
| prompt = expandPromptPattern(secondaryPromptPattern, 0, missing, line + 1); |
| width = Math.max(width, prompt.columnLength()); |
| } |
| buf.setLength(0); |
| } |
| int line = 0; |
| while (line < lines.size() - 1) { |
| sb.append(lines.get(line)).append("\n"); |
| buf.append(lines.get(line)).append("\n"); |
| AttributedString prompt; |
| if (computePrompts) { |
| String missing = ""; |
| if (needsMessage) { |
| if (missings.isEmpty()) { |
| try { |
| parser.parse(buf.toString(), buf.length(), ParseContext.SECONDARY_PROMPT); |
| } catch (EOFError e) { |
| missing = e.getMissing(); |
| } catch (SyntaxError e) { |
| // Ignore |
| } |
| } else { |
| missing = missings.get(line); |
| } |
| } |
| prompt = expandPromptPattern(secondaryPromptPattern, width, missing, line + 1); |
| } else { |
| prompt = prompts.get(line); |
| } |
| prompts.add(prompt); |
| sb.append(prompt); |
| line++; |
| } |
| sb.append(lines.get(line)); |
| buf.append(lines.get(line)); |
| return sb.toAttributedString(); |
| } |
| |
| private AttributedString addRightPrompt(AttributedString prompt, AttributedString line) { |
| int width = prompt.columnLength(); |
| boolean endsWithNl = line.length() > 0 |
| && line.charAt(line.length() - 1) == '\n'; |
| // columnLength counts -1 for the final newline; adjust for that |
| int nb = size.getColumns() - width |
| - (line.columnLength() + (endsWithNl ? 1 : 0)); |
| if (nb >= 3) { |
| AttributedStringBuilder sb = new AttributedStringBuilder(size.getColumns()); |
| sb.append(line, 0, endsWithNl ? line.length() - 1 : line.length()); |
| for (int j = 0; j < nb; j++) { |
| sb.append(' '); |
| } |
| sb.append(prompt); |
| if (endsWithNl) { |
| sb.append('\n'); |
| } |
| line = sb.toAttributedString(); |
| } |
| return line; |
| } |
| |
| // |
| // Completion |
| // |
| |
| protected boolean insertTab() { |
| return isSet(Option.INSERT_TAB) |
| && getLastBinding().equals("\t") |
| && buf.toString().matches("(^|[\\s\\S]*\n)[\r\n\t ]*"); |
| } |
| |
| protected boolean expandHistory() { |
| String str = buf.toString(); |
| String exp = expander.expandHistory(history, str); |
| if (!exp.equals(str)) { |
| buf.clear(); |
| buf.write(exp); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| protected enum CompletionType { |
| Expand, |
| ExpandComplete, |
| Complete, |
| List, |
| } |
| |
| protected boolean expandWord() { |
| if (insertTab()) { |
| return selfInsert(); |
| } else { |
| return doComplete(CompletionType.Expand, isSet(Option.MENU_COMPLETE), false); |
| } |
| } |
| |
| protected boolean expandOrComplete() { |
| if (insertTab()) { |
| return selfInsert(); |
| } else { |
| return doComplete(CompletionType.ExpandComplete, isSet(Option.MENU_COMPLETE), false); |
| } |
| } |
| |
| protected boolean expandOrCompletePrefix() { |
| if (insertTab()) { |
| return selfInsert(); |
| } else { |
| return doComplete(CompletionType.ExpandComplete, isSet(Option.MENU_COMPLETE), true); |
| } |
| } |
| |
| protected boolean completeWord() { |
| if (insertTab()) { |
| return selfInsert(); |
| } else { |
| return doComplete(CompletionType.Complete, isSet(Option.MENU_COMPLETE), false); |
| } |
| } |
| |
| protected boolean menuComplete() { |
| if (insertTab()) { |
| return selfInsert(); |
| } else { |
| return doComplete(CompletionType.Complete, true, false); |
| } |
| } |
| |
| protected boolean menuExpandOrComplete() { |
| if (insertTab()) { |
| return selfInsert(); |
| } else { |
| return doComplete(CompletionType.ExpandComplete, true, false); |
| } |
| } |
| |
| protected boolean completePrefix() { |
| if (insertTab()) { |
| return selfInsert(); |
| } else { |
| return doComplete(CompletionType.Complete, isSet(Option.MENU_COMPLETE), true); |
| } |
| } |
| |
| protected boolean listChoices() { |
| return listChoices(false); |
| } |
| |
| private boolean listChoices(boolean forSuggestion) { |
| return doComplete(CompletionType.List, isSet(Option.MENU_COMPLETE), false, forSuggestion); |
| } |
| |
| protected boolean deleteCharOrList() { |
| if (buf.cursor() != buf.length() || buf.length() == 0) { |
| return deleteChar(); |
| } else { |
| return doComplete(CompletionType.List, isSet(Option.MENU_COMPLETE), false); |
| } |
| } |
| |
| protected boolean doComplete(CompletionType lst, boolean useMenu, boolean prefix) { |
| return doComplete(lst, useMenu, prefix, false); |
| } |
| |
| protected boolean doComplete(CompletionType lst, boolean useMenu, boolean prefix, boolean forSuggestion) { |
| // If completion is disabled, just bail out |
| if (getBoolean(DISABLE_COMPLETION, false)) { |
| return true; |
| } |
| // Try to expand history first |
| // If there is actually an expansion, bail out now |
| if (!isSet(Option.DISABLE_EVENT_EXPANSION)) { |
| try { |
| if (expandHistory()) { |
| return true; |
| } |
| } catch (Exception e) { |
| Log.info("Error while expanding history", e); |
| return false; |
| } |
| } |
| |
| // Parse the command line |
| CompletingParsedLine line; |
| try { |
| line = wrap(parser.parse(buf.toString(), buf.cursor(), ParseContext.COMPLETE)); |
| } catch (Exception e) { |
| Log.info("Error while parsing line", e); |
| return false; |
| } |
| |
| // Find completion candidates |
| List<Candidate> candidates = new ArrayList<>(); |
| try { |
| if (completer != null) { |
| completer.complete(this, line, candidates); |
| } |
| } catch (Exception e) { |
| Log.info("Error while finding completion candidates", e); |
| return false; |
| } |
| |
| if (lst == CompletionType.ExpandComplete || lst == CompletionType.Expand) { |
| String w = expander.expandVar(line.word()); |
| if (!line.word().equals(w)) { |
| if (prefix) { |
| buf.backspace(line.wordCursor()); |
| } else { |
| buf.move(line.word().length() - line.wordCursor()); |
| buf.backspace(line.word().length()); |
| } |
| buf.write(w); |
| return true; |
| } |
| if (lst == CompletionType.Expand) { |
| return false; |
| } else { |
| lst = CompletionType.Complete; |
| } |
| } |
| |
| boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE); |
| int errors = getInt(ERRORS, DEFAULT_ERRORS); |
| |
| // Build a list of sorted candidates |
| Map<String, List<Candidate>> sortedCandidates = new HashMap<>(); |
| for (Candidate cand : candidates) { |
| sortedCandidates |
| .computeIfAbsent(AttributedString.fromAnsi(cand.value()).toString(), s -> new ArrayList<>()) |
| .add(cand); |
| } |
| |
| // Find matchers |
| // TODO: glob completion |
| List<Function<Map<String, List<Candidate>>, |
| Map<String, List<Candidate>>>> matchers; |
| Predicate<String> exact; |
| if (prefix) { |
| String wd = line.word(); |
| String wdi = caseInsensitive ? wd.toLowerCase() : wd; |
| String wp = wdi.substring(0, line.wordCursor()); |
| matchers = Arrays.asList( |
| simpleMatcher(s -> (caseInsensitive ? s.toLowerCase() : s).startsWith(wp)), |
| simpleMatcher(s -> (caseInsensitive ? s.toLowerCase() : s).contains(wp)), |
| typoMatcher(wp, errors, caseInsensitive) |
| ); |
| exact = s -> caseInsensitive ? s.equalsIgnoreCase(wp) : s.equals(wp); |
| } else if (isSet(Option.COMPLETE_IN_WORD)) { |
| String wd = line.word(); |
| String wdi = caseInsensitive ? wd.toLowerCase() : wd; |
| String wp = wdi.substring(0, line.wordCursor()); |
| String ws = wdi.substring(line.wordCursor()); |
| Pattern p1 = Pattern.compile(Pattern.quote(wp) + ".*" + Pattern.quote(ws) + ".*"); |
| Pattern p2 = Pattern.compile(".*" + Pattern.quote(wp) + ".*" + Pattern.quote(ws) + ".*"); |
| matchers = Arrays.asList( |
| simpleMatcher(s -> p1.matcher(caseInsensitive ? s.toLowerCase() : s).matches()), |
| simpleMatcher(s -> p2.matcher(caseInsensitive ? s.toLowerCase() : s).matches()), |
| typoMatcher(wdi, errors, caseInsensitive) |
| ); |
| exact = s -> caseInsensitive ? s.equalsIgnoreCase(wd) : s.equals(wd); |
| } else { |
| String wd = line.word(); |
| String wdi = caseInsensitive ? wd.toLowerCase() : wd; |
| if (isSet(Option.EMPTY_WORD_OPTIONS) || wd.length() > 0) { |
| matchers = Arrays.asList( |
| simpleMatcher(s -> (caseInsensitive ? s.toLowerCase() : s).startsWith(wdi)), |
| simpleMatcher(s -> (caseInsensitive ? s.toLowerCase() : s).contains(wdi)), |
| typoMatcher(wdi, errors, caseInsensitive) |
| ); |
| } else { |
| matchers = Arrays.asList( |
| simpleMatcher(s -> !s.startsWith("-")) |
| ); |
| } |
| exact = s -> caseInsensitive ? s.equalsIgnoreCase(wd) : s.equals(wd); |
| } |
| // Find matching candidates |
| Map<String, List<Candidate>> matching = Collections.emptyMap(); |
| for (Function<Map<String, List<Candidate>>, |
| Map<String, List<Candidate>>> matcher : matchers) { |
| matching = matcher.apply(sortedCandidates); |
| if (!matching.isEmpty()) { |
| break; |
| } |
| } |
| |
| // If we have no matches, bail out |
| if (matching.isEmpty()) { |
| return false; |
| } |
| 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, forSuggestion); |
| 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()); |
| |
| 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)); |
| callWidget(REDISPLAY); |
| 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) { |
| if (line instanceof CompletingParsedLine) { |
| return (CompletingParsedLine) line; |
| } else { |
| return new CompletingParsedLine() { |
| public String word() { |
| return line.word(); |
| } |
| public int wordCursor() { |
| return line.wordCursor(); |
| } |
| public int wordIndex() { |
| return line.wordIndex(); |
| } |
| public List<String> words() { |
| return line.words(); |
| } |
| public String line() { |
| return line.line(); |
| } |
| public int cursor() { |
| return line.cursor(); |
| } |
| public CharSequence escape(CharSequence candidate, boolean complete) { |
| return candidate; |
| } |
| public int rawWordCursor() { |
| return wordCursor(); |
| } |
| public int rawWordLength() { |
| return word().length(); |
| } |
| }; |
| } |
| } |
| |
| protected Comparator<Candidate> getCandidateComparator(boolean caseInsensitive, String word) { |
| String wdi = caseInsensitive ? word.toLowerCase() : word; |
| ToIntFunction<String> wordDistance = w -> distance(wdi, caseInsensitive ? w.toLowerCase() : w); |
| return Comparator |
| .comparing(Candidate::value, Comparator.comparingInt(wordDistance)) |
| .thenComparing(Comparator.naturalOrder()); |
| } |
| |
| protected String getOthersGroupName() { |
| return getString(OTHERS_GROUP_NAME, DEFAULT_OTHERS_GROUP_NAME); |
| } |
| |
| protected String getOriginalGroupName() { |
| return getString(ORIGINAL_GROUP_NAME, DEFAULT_ORIGINAL_GROUP_NAME); |
| } |
| |
| |
| protected Comparator<String> getGroupComparator() { |
| return Comparator.<String>comparingInt(s -> getOthersGroupName().equals(s) ? 1 : getOriginalGroupName().equals(s) ? -1 : 0) |
| .thenComparing(String::toLowerCase, Comparator.naturalOrder()); |
| } |
| |
| private void mergeCandidates(List<Candidate> possible) { |
| // Merge candidates if the have the same key |
| Map<String, List<Candidate>> keyedCandidates = new HashMap<>(); |
| for (Candidate candidate : possible) { |
| if (candidate.key() != null) { |
| List<Candidate> cands = keyedCandidates.computeIfAbsent(candidate.key(), s -> new ArrayList<>()); |
| cands.add(candidate); |
| } |
| } |
| if (!keyedCandidates.isEmpty()) { |
| for (List<Candidate> candidates : keyedCandidates.values()) { |
| if (candidates.size() >= 1) { |
| possible.removeAll(candidates); |
| // Candidates with the same key are supposed to have |
| // the same description |
| candidates.sort(Comparator.comparing(Candidate::value)); |
| Candidate first = candidates.get(0); |
| String disp = candidates.stream() |
| .map(Candidate::displ) |
| .collect(Collectors.joining(" ")); |
| possible.add(new Candidate(first.value(), disp, first.group(), |
| first.descr(), first.suffix(), null, first.complete())); |
| } |
| } |
| } |
| } |
| |
| private Function<Map<String, List<Candidate>>, |
| Map<String, List<Candidate>>> simpleMatcher(Predicate<String> pred) { |
| return m -> m.entrySet().stream() |
| .filter(e -> pred.test(e.getKey())) |
| .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); |
| } |
| |
| private Function<Map<String, List<Candidate>>, |
| Map<String, List<Candidate>>> typoMatcher(String word, int errors, boolean caseInsensitive) { |
| return m -> { |
| Map<String, List<Candidate>> map = m.entrySet().stream() |
| .filter(e -> distance(word, caseInsensitive ? e.getKey() : e.getKey().toLowerCase()) < errors) |
| .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); |
| if (map.size() > 1) { |
| map.computeIfAbsent(word, w -> new ArrayList<>()) |
| .add(new Candidate(word, word, getOriginalGroupName(), null, null, null, false)); |
| } |
| return map; |
| }; |
| } |
| |
| private int distance(String word, String cand) { |
| if (word.length() < cand.length()) { |
| int d1 = Levenshtein.distance(word, cand.substring(0, Math.min(cand.length(), word.length()))); |
| int d2 = Levenshtein.distance(word, cand); |
| return Math.min(d1, d2); |
| } else { |
| return Levenshtein.distance(word, cand); |
| } |
| } |
| |
| protected boolean nextBindingIsComplete() { |
| redisplay(); |
| KeyMap<Binding> keyMap = keyMaps.get(MENU); |
| Binding operation = readBinding(getKeys(), keyMap); |
| if (operation instanceof Reference && MENU_COMPLETE.equals(((Reference) operation).name())) { |
| return true; |
| } else { |
| pushBackBinding(); |
| return false; |
| } |
| } |
| |
| private class MenuSupport implements Supplier<AttributedString> { |
| final List<Candidate> possible; |
| final BiFunction<CharSequence, Boolean, CharSequence> escaper; |
| int selection; |
| int topLine; |
| String word; |
| AttributedString computed; |
| int lines; |
| int columns; |
| String completed; |
| |
| public MenuSupport(List<Candidate> original, String completed, BiFunction<CharSequence, Boolean, CharSequence> escaper) { |
| this.possible = new ArrayList<>(); |
| this.escaper = escaper; |
| this.selection = -1; |
| this.topLine = 0; |
| this.word = ""; |
| this.completed = completed; |
| computePost(original, null, possible, completed); |
| next(); |
| } |
| |
| public Candidate completion() { |
| return possible.get(selection); |
| } |
| |
| public void next() { |
| selection = (selection + 1) % possible.size(); |
| update(); |
| } |
| |
| public void previous() { |
| selection = (selection + possible.size() - 1) % possible.size(); |
| update(); |
| } |
| |
| /** |
| * Move 'step' options along the major axis of the menu.<p> |
| * ie. if the menu is listing rows first, change row (up/down); |
| * otherwise move column (left/right) |
| * |
| * @param step number of options to move by |
| */ |
| private void major(int step) { |
| int axis = isSet(Option.LIST_ROWS_FIRST) ? columns : lines; |
| int sel = selection + step * axis; |
| if (sel < 0) { |
| int pos = (sel + axis) % axis; // needs +axis as (-1)%x == -1 |
| int remainders = possible.size() % axis; |
| sel = possible.size() - remainders + pos; |
| if (sel >= possible.size()) { |
| sel -= axis; |
| } |
| } else if (sel >= possible.size()) { |
| sel = sel % axis; |
| } |
| selection = sel; |
| update(); |
| } |
| |
| /** |
| * Move 'step' options along the minor axis of the menu.<p> |
| * ie. if the menu is listing rows first, move along the row (left/right); |
| * otherwise move along the column (up/down) |
| * |
| * @param step number of options to move by |
| */ |
| private void minor(int step) { |
| int axis = isSet(Option.LIST_ROWS_FIRST) ? columns : lines; |
| int row = selection % axis; |
| int options = possible.size(); |
| if (selection - row + axis > options) { |
| // selection is the last row/column |
| // so there are fewer options than other rows |
| axis = options%axis; |
| } |
| selection = selection - row + ((axis + row + step) % axis); |
| update(); |
| } |
| |
| public void up() { |
| if (isSet(Option.LIST_ROWS_FIRST)) { |
| major(-1); |
| } else { |
| minor(-1); |
| } |
| } |
| |
| public void down() { |
| if (isSet(Option.LIST_ROWS_FIRST)) { |
| major(1); |
| } else { |
| minor(1); |
| } |
| } |
| |
| public void left() { |
| if (isSet(Option.LIST_ROWS_FIRST)) { |
| minor(-1); |
| } else { |
| major(-1); |
| } |
| } |
| |
| public void right() { |
| if (isSet(Option.LIST_ROWS_FIRST)) { |
| minor(1); |
| } else { |
| major(1); |
| } |
| } |
| |
| private void update() { |
| buf.backspace(word.length()); |
| word = escaper.apply(completion().value(), true).toString(); |
| buf.write(word); |
| |
| // Compute displayed prompt |
| PostResult pr = computePost(possible, completion(), null, completed); |
| AttributedString text = insertSecondaryPrompts(AttributedStringBuilder.append(prompt, buf.toString()), new ArrayList<>()); |
| int promptLines = text.columnSplitLength(size.getColumns(), false, display.delayLineWrap()).size(); |
| Status status = Status.getStatus(terminal, false); |
| int displaySize = size.getRows() - (status != null ? status.size() : 0) - promptLines; |
| if (pr.lines > displaySize) { |
| int displayed = displaySize - 1; |
| if (pr.selectedLine >= 0) { |
| if (pr.selectedLine < topLine) { |
| topLine = pr.selectedLine; |
| } else if (pr.selectedLine >= topLine + displayed) { |
| topLine = pr.selectedLine - displayed + 1; |
| } |
| } |
| AttributedString post = pr.post; |
| if (post.length() > 0 && post.charAt(post.length() - 1) != '\n') { |
| post = new AttributedStringBuilder(post.length() + 1) |
| .append(post).append("\n").toAttributedString(); |
| } |
| List<AttributedString> lines = post.columnSplitLength(size.getColumns(), true, display.delayLineWrap()); |
| List<AttributedString> sub = new ArrayList<>(lines.subList(topLine, topLine + displayed)); |
| sub.add(new AttributedStringBuilder() |
| .style(AttributedStyle.DEFAULT.foreground(AttributedStyle.CYAN)) |
| .append("rows ") |
| .append(Integer.toString(topLine + 1)) |
| .append(" to ") |
| .append(Integer.toString(topLine + displayed)) |
| .append(" of ") |
| .append(Integer.toString(lines.size())) |
| .append("\n") |
| .style(AttributedStyle.DEFAULT).toAttributedString()); |
| computed = AttributedString.join(AttributedString.EMPTY, sub); |
| } else { |
| computed = pr.post; |
| } |
| lines = pr.lines; |
| columns = (possible.size() + lines - 1) / lines; |
| } |
| |
| @Override |
| public AttributedString get() { |
| return computed; |
| } |
| |
| } |
| |
| protected boolean doMenu(List<Candidate> original, String completed, BiFunction<CharSequence, Boolean, CharSequence> escaper) { |
| // Reorder candidates according to display order |
| final List<Candidate> possible = new ArrayList<>(); |
| boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE); |
| original.sort(getCandidateComparator(caseInsensitive, completed)); |
| mergeCandidates(original); |
| computePost(original, null, possible, completed); |
| |
| // Build menu support |
| MenuSupport menuSupport = new MenuSupport(original, completed, escaper); |
| post = menuSupport; |
| callWidget(REDISPLAY); |
| |
| // Loop |
| KeyMap<Binding> keyMap = keyMaps.get(MENU); |
| Binding operation; |
| while ((operation = readBinding(getKeys(), keyMap)) != null) { |
| String ref = (operation instanceof Reference) ? ((Reference) operation).name() : ""; |
| switch (ref) { |
| case MENU_COMPLETE: |
| menuSupport.next(); |
| break; |
| case REVERSE_MENU_COMPLETE: |
| menuSupport.previous(); |
| break; |
| case UP_LINE_OR_HISTORY: |
| case UP_LINE_OR_SEARCH: |
| menuSupport.up(); |
| break; |
| case DOWN_LINE_OR_HISTORY: |
| case DOWN_LINE_OR_SEARCH: |
| menuSupport.down(); |
| break; |
| case FORWARD_CHAR: |
| menuSupport.right(); |
| break; |
| case BACKWARD_CHAR: |
| menuSupport.left(); |
| break; |
| case CLEAR_SCREEN: |
| clearScreen(); |
| break; |
| default: { |
| Candidate completion = menuSupport.completion(); |
| if (completion.suffix() != null) { |
| String chars = getString(REMOVE_SUFFIX_CHARS, DEFAULT_REMOVE_SUFFIX_CHARS); |
| if (SELF_INSERT.equals(ref) |
| && chars.indexOf(getLastBinding().charAt(0)) >= 0 |
| || BACKWARD_DELETE_CHAR.equals(ref)) { |
| buf.backspace(completion.suffix().length()); |
| } |
| } |
| if (completion.complete() |
| && getLastBinding().charAt(0) != ' ' |
| && (SELF_INSERT.equals(ref) || getLastBinding().charAt(0) != ' ')) { |
| buf.write(' '); |
| } |
| if (!ACCEPT_LINE.equals(ref) |
| && !(SELF_INSERT.equals(ref) |
| && completion.suffix() != null |
| && completion.suffix().startsWith(getLastBinding()))) { |
| pushBackBinding(true); |
| } |
| post = null; |
| return true; |
| } |
| } |
| doAutosuggestion = false; |
| callWidget(REDISPLAY); |
| } |
| return false; |
| } |
| |
| protected boolean clearChoices() { |
| return doList(new ArrayList<Candidate>(), "", false, null, false); |
| } |
| |
| protected boolean doList(List<Candidate> possible |
| , String completed, boolean runLoop, BiFunction<CharSequence, Boolean, CharSequence> escaper) { |
| return doList(possible, completed, runLoop, escaper, false); |
| } |
| |
| protected boolean doList(List<Candidate> possible |
| , String completed |
| , boolean runLoop, BiFunction<CharSequence, Boolean, CharSequence> escaper, boolean forSuggestion) { |
| // If we list only and if there's a big |
| // number of items, we should ask the user |
| // for confirmation, display the list |
| // and redraw the line at the bottom |
| mergeCandidates(possible); |
| AttributedString text = insertSecondaryPrompts(AttributedStringBuilder.append(prompt, buf.toString()), new ArrayList<>()); |
| int promptLines = text.columnSplitLength(size.getColumns(), false, display.delayLineWrap()).size(); |
| PostResult postResult = computePost(possible, null, null, completed); |
| int lines = postResult.lines; |
| int listMax = getInt(LIST_MAX, DEFAULT_LIST_MAX); |
| if (listMax > 0 && possible.size() >= listMax |
| || lines >= size.getRows() - promptLines) { |
| if (!forSuggestion) { |
| // prompt |
| post = () -> new AttributedString(getAppName() + ": do you wish to see all " + possible.size() |
| + " possibilities (" + lines + " lines)?"); |
| redisplay(true); |
| int c = readCharacter(); |
| if (c != 'y' && c != 'Y' && c != '\t') { |
| post = null; |
| return false; |
| } |
| } else { |
| return false; |
| } |
| } |
| |
| boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE); |
| StringBuilder sb = new StringBuilder(); |
| while (true) { |
| String current = completed + sb.toString(); |
| List<Candidate> cands; |
| if (sb.length() > 0) { |
| cands = possible.stream() |
| .filter(c -> caseInsensitive |
| ? c.value().toLowerCase().startsWith(current.toLowerCase()) |
| : c.value().startsWith(current)) |
| .sorted(getCandidateComparator(caseInsensitive, current)) |
| .collect(Collectors.toList()); |
| } else { |
| cands = possible.stream() |
| .sorted(getCandidateComparator(caseInsensitive, current)) |
| .collect(Collectors.toList()); |
| } |
| post = () -> { |
| AttributedString t = insertSecondaryPrompts(AttributedStringBuilder.append(prompt, buf.toString()), new ArrayList<>()); |
| int pl = t.columnSplitLength(size.getColumns(), false, display.delayLineWrap()).size(); |
| PostResult pr = computePost(cands, null, null, current); |
| if (pr.lines >= size.getRows() - pl) { |
| post = null; |
| int oldCursor = buf.cursor(); |
| buf.cursor(buf.length()); |
| redisplay(false); |
| buf.cursor(oldCursor); |
| println(); |
| List<AttributedString> ls = postResult.post.columnSplitLength(size.getColumns(), false, display.delayLineWrap()); |
| Display d = new Display(terminal, false); |
| d.resize(size.getRows(), size.getColumns()); |
| d.update(ls, -1); |
| redrawLine(); |
| return new AttributedString(""); |
| } |
| return pr.post; |
| }; |
| if (!runLoop) { |
| return false; |
| } |
| redisplay(); |
| // TODO: use a different keyMap ? |
| 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)) { |
| if (sb.length() == 0) { |
| pushBackBinding(); |
| post = null; |
| return false; |
| } else { |
| sb.setLength(sb.length() - 1); |
| buf.backspace(); |
| } |
| } else if (SELF_INSERT.equals(name)) { |
| sb.append(getLastBinding()); |
| callWidget(name); |
| if (cands.isEmpty()) { |
| post = null; |
| return false; |
| } |
| } else if ("\t".equals(getLastBinding())) { |
| if (cands.size() == 1 || sb.length() > 0) { |
| post = null; |
| pushBackBinding(); |
| } else if (isSet(Option.AUTO_MENU)) { |
| buf.backspace(escaper.apply(current, false).length()); |
| doMenu(cands, current, escaper); |
| } |
| return false; |
| } else { |
| pushBackBinding(); |
| post = null; |
| return false; |
| } |
| } else if (b == null) { |
| post = null; |
| return false; |
| } |
| } |
| } |
| |
| protected static class PostResult { |
| final AttributedString post; |
| final int lines; |
| final int selectedLine; |
| |
| public PostResult(AttributedString post, int lines, int selectedLine) { |
| this.post = post; |
| this.lines = lines; |
| this.selectedLine = selectedLine; |
| } |
| } |
| |
| protected PostResult computePost(List<Candidate> possible, Candidate selection, List<Candidate> ordered, String completed) { |
| return computePost(possible, selection, ordered, completed, display::wcwidth, size.getColumns(), isSet(Option.AUTO_GROUP), isSet(Option.GROUP), isSet(Option.LIST_ROWS_FIRST)); |
| } |
| |
| protected PostResult computePost(List<Candidate> possible, Candidate selection, List<Candidate> ordered, String completed, Function<String, Integer> wcwidth, int width, boolean autoGroup, boolean groupName, boolean rowsFirst) { |
| List<Object> strings = new ArrayList<>(); |
| if (groupName) { |
| Comparator<String> groupComparator = getGroupComparator(); |
| Map<String, Map<String, Candidate>> sorted; |
| sorted = groupComparator != null |
| ? new TreeMap<>(groupComparator) |
| : new LinkedHashMap<>(); |
| for (Candidate cand : possible) { |
| String group = cand.group(); |
| sorted.computeIfAbsent(group != null ? group : "", s -> new LinkedHashMap<>()) |
| .put(cand.value(), cand); |
| } |
| for (Map.Entry<String, Map<String, Candidate>> entry : sorted.entrySet()) { |
| String group = entry.getKey(); |
| if (group.isEmpty() && sorted.size() > 1) { |
| group = getOthersGroupName(); |
| } |
| if (!group.isEmpty() && autoGroup) { |
| strings.add(group); |
| } |
| strings.add(new ArrayList<>(entry.getValue().values())); |
| if (ordered != null) { |
| ordered.addAll(entry.getValue().values()); |
| } |
| } |
| } else { |
| Set<String> groups = new LinkedHashSet<>(); |
| TreeMap<String, Candidate> sorted = new TreeMap<>(); |
| for (Candidate cand : possible) { |
| String group = cand.group(); |
| if (group != null) { |
| groups.add(group); |
| } |
| sorted.put(cand.value(), cand); |
| } |
| if (autoGroup) { |
| strings.addAll(groups); |
| } |
| strings.add(new ArrayList<>(sorted.values())); |
| if (ordered != null) { |
| ordered.addAll(sorted.values()); |
| } |
| } |
| return toColumns(strings, selection, completed, wcwidth, width, rowsFirst); |
| } |
| |
| private static final String DESC_PREFIX = "("; |
| private static final String DESC_SUFFIX = ")"; |
| private static final int MARGIN_BETWEEN_DISPLAY_AND_DESC = 1; |
| private static final int MARGIN_BETWEEN_COLUMNS = 3; |
| |
| @SuppressWarnings("unchecked") |
| protected PostResult toColumns(List<Object> items, Candidate selection, String completed, Function<String, Integer> wcwidth, int width, boolean rowsFirst) { |
| int[] out = new int[2]; |
| // TODO: support Option.LIST_PACKED |
| // Compute column width |
| int maxWidth = 0; |
| for (Object item : items) { |
| if (item instanceof String) { |
| int len = wcwidth.apply((String) item); |
| maxWidth = Math.max(maxWidth, len); |
| } |
| else if (item instanceof List) { |
| for (Candidate cand : (List<Candidate>) item) { |
| int len = wcwidth.apply(cand.displ()); |
| if (cand.descr() != null) { |
| len += MARGIN_BETWEEN_DISPLAY_AND_DESC; |
| len += DESC_PREFIX.length(); |
| len += wcwidth.apply(cand.descr()); |
| len += DESC_SUFFIX.length(); |
| } |
| maxWidth = Math.max(maxWidth, len); |
| } |
| } |
| } |
| // Build columns |
| AttributedStringBuilder sb = new AttributedStringBuilder(); |
| for (Object list : items) { |
| toColumns(list, width, maxWidth, sb, selection, completed, rowsFirst, out); |
| } |
| if (sb.length() > 0 && sb.charAt(sb.length() - 1) == '\n') { |
| sb.setLength(sb.length() - 1); |
| } |
| return new PostResult(sb.toAttributedString(), out[0], out[1]); |
| } |
| |
| @SuppressWarnings("unchecked") |
| protected void toColumns(Object items, int width, int maxWidth, AttributedStringBuilder sb, Candidate selection, String completed, boolean rowsFirst, int[] out) { |
| if (maxWidth <= 0 || width <= 0) { |
| return; |
| } |
| // This is a group |
| if (items instanceof String) { |
| sb.style(getCompletionStyleGroup()) |
| .append((String) items) |
| .style(AttributedStyle.DEFAULT) |
| .append("\n"); |
| out[0]++; |
| } |
| // This is a Candidate list |
| else if (items instanceof List) { |
| List<Candidate> candidates = (List<Candidate>) items; |
| maxWidth = Math.min(width, maxWidth); |
| int c = width / maxWidth; |
| while (c > 1 && c * maxWidth + (c - 1) * MARGIN_BETWEEN_COLUMNS >= width) { |
| c--; |
| } |
| int lines = (candidates.size() + c - 1) / c; |
| // Try to minimize the number of columns for the given number of rows |
| // Prevents eg 9 candiates being split 6/3 instead of 5/4. |
| final int columns = (candidates.size() + lines - 1) / lines; |
| IntBinaryOperator index; |
| if (rowsFirst) { |
| index = (i, j) -> i * columns + j; |
| } else { |
| index = (i, j) -> j * lines + i; |
| } |
| for (int i = 0; i < lines; i++) { |
| for (int j = 0; j < columns; j++) { |
| int idx = index.applyAsInt(i, j); |
| if (idx < candidates.size()) { |
| Candidate cand = candidates.get(idx); |
| boolean hasRightItem = j < columns - 1 && index.applyAsInt(i, j + 1) < candidates.size(); |
| AttributedString left = AttributedString.fromAnsi(cand.displ()); |
| AttributedString right = AttributedString.fromAnsi(cand.descr()); |
| int lw = left.columnLength(); |
| int rw = 0; |
| if (right != null) { |
| int rem = maxWidth - (lw + MARGIN_BETWEEN_DISPLAY_AND_DESC |
| + DESC_PREFIX.length() + DESC_SUFFIX.length()); |
| rw = right.columnLength(); |
| if (rw > rem) { |
| right = AttributedStringBuilder.append( |
| right.columnSubSequence(0, rem - WCWidth.wcwidth('\u2026')), |
| "\u2026"); |
| rw = right.columnLength(); |
| } |
| right = AttributedStringBuilder.append(DESC_PREFIX, right, DESC_SUFFIX); |
| rw += DESC_PREFIX.length() + DESC_SUFFIX.length(); |
| } |
| if (cand == selection) { |
| out[1] = i; |
| sb.style(getCompletionStyleSelection()); |
| if (left.toString().regionMatches( |
| isSet(Option.CASE_INSENSITIVE), 0, completed, 0, completed.length())) { |
| sb.append(left.toString(), 0, completed.length()); |
| sb.append(left.toString(), completed.length(), left.length()); |
| } else { |
| sb.append(left.toString()); |
| } |
| for (int k = 0; k < maxWidth - lw - rw; k++) { |
| sb.append(' '); |
| } |
| if (right != null) { |
| sb.append(right); |
| } |
| sb.style(AttributedStyle.DEFAULT); |
| } else { |
| if (left.toString().regionMatches( |
| isSet(Option.CASE_INSENSITIVE), 0, completed, 0, completed.length())) { |
| sb.style(getCompletionStyleStarting()); |
| sb.append(left, 0, completed.length()); |
| sb.style(AttributedStyle.DEFAULT); |
| sb.append(left, completed.length(), left.length()); |
| } else { |
| sb.append(left); |
| } |
| if (right != null || hasRightItem) { |
| for (int k = 0; k < maxWidth - lw - rw; k++) { |
| sb.append(' '); |
| } |
| } |
| if (right != null) { |
| sb.style(getCompletionStyleDescription()); |
| sb.append(right); |
| sb.style(AttributedStyle.DEFAULT); |
| } |
| } |
| if (hasRightItem) { |
| for (int k = 0; k < MARGIN_BETWEEN_COLUMNS; k++) { |
| sb.append(' '); |
| } |
| } |
| } |
| } |
| sb.append('\n'); |
| } |
| out[0] += lines; |
| } |
| } |
| |
| private AttributedStyle getCompletionStyleStarting() { |
| return getCompletionStyle(COMPLETION_STYLE_STARTING, DEFAULT_COMPLETION_STYLE_STARTING); |
| } |
| |
| protected AttributedStyle getCompletionStyleDescription() { |
| return getCompletionStyle(COMPLETION_STYLE_DESCRIPTION, DEFAULT_COMPLETION_STYLE_DESCRIPTION); |
| } |
| |
| protected AttributedStyle getCompletionStyleGroup() { |
| return getCompletionStyle(COMPLETION_STYLE_GROUP, DEFAULT_COMPLETION_STYLE_GROUP); |
| } |
| |
| protected AttributedStyle getCompletionStyleSelection() { |
| return getCompletionStyle(COMPLETION_STYLE_SELECTION, DEFAULT_COMPLETION_STYLE_SELECTION); |
| } |
| |
| protected AttributedStyle getCompletionStyle(String name, String value) { |
| return buildStyle(getString(name, value)); |
| } |
| |
| protected AttributedStyle buildStyle(String str) { |
| return AttributedString.fromAnsi("\u001b[" + str + "m ").styleAt(0); |
| } |
| |
| private String getCommonStart(String str1, String str2, boolean caseInsensitive) { |
| int[] s1 = str1.codePoints().toArray(); |
| int[] s2 = str2.codePoints().toArray(); |
| int len = 0; |
| while (len < Math.min(s1.length, s2.length)) { |
| int ch1 = s1[len]; |
| int ch2 = s2[len]; |
| if (ch1 != ch2 && caseInsensitive) { |
| ch1 = Character.toUpperCase(ch1); |
| ch2 = Character.toUpperCase(ch2); |
| if (ch1 != ch2) { |
| ch1 = Character.toLowerCase(ch1); |
| ch2 = Character.toLowerCase(ch2); |
| } |
| } |
| if (ch1 != ch2) { |
| break; |
| } |
| len++; |
| } |
| return new String(s1, 0, len); |
| } |
| |
| /** |
| * 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 |
| */ |
| protected boolean moveHistory(final boolean next, int count) { |
| boolean ok = true; |
| for (int i = 0; i < count && (ok = moveHistory(next)); i++) { |
| /* empty */ |
| } |
| return ok; |
| } |
| |
| /** |
| * Move up or down the history tree. |
| * @param next <code>true</code> to go to the next, <code>false</code> for the previous. |
| * @return <code>true</code> if successful, <code>false</code> otherwise |
| */ |
| protected boolean moveHistory(final boolean next) { |
| if (!buf.toString().equals(history.current())) { |
| modifiedHistory.put(history.index(), buf.toString()); |
| } |
| if (next && !history.next()) { |
| return false; |
| } |
| else if (!next && !history.previous()) { |
| return false; |
| } |
| |
| setBuffer(modifiedHistory.containsKey(history.index()) |
| ? modifiedHistory.get(history.index()) |
| : history.current()); |
| |
| return true; |
| } |
| |
| // |
| // Printing |
| // |
| |
| /** |
| * Raw output printing. |
| * @param str the string to print to the terminal |
| */ |
| void print(String str) { |
| terminal.writer().write(str); |
| } |
| |
| void println(String s) { |
| print(s); |
| println(); |
| } |
| |
| /** |
| * Output a platform-dependant newline. |
| */ |
| void println() { |
| terminal.puts(Capability.carriage_return); |
| print("\n"); |
| redrawLine(); |
| } |
| |
| |
| // |
| // Actions |
| // |
| |
| protected boolean killBuffer() { |
| killRing.add(buf.toString()); |
| buf.clear(); |
| return true; |
| } |
| |
| protected boolean killWholeLine() { |
| if (buf.length() == 0) { |
| return false; |
| } |
| int start; |
| int end; |
| if (count < 0) { |
| end = buf.cursor(); |
| while (buf.atChar(end) != 0 && buf.atChar(end) != '\n') { |
| end++; |
| } |
| start = end; |
| for (int count = -this.count; count > 0; --count) { |
| while (start > 0 && buf.atChar(start - 1) != '\n') { |
| start--; |
| } |
| start--; |
| } |
| } else { |
| start = buf.cursor(); |
| while (start > 0 && buf.atChar(start - 1) != '\n') { |
| start--; |
| } |
| end = start; |
| while (count-- > 0) { |
| while (end < buf.length() && buf.atChar(end) != '\n') { |
| end++; |
| } |
| if (end < buf.length()) { |
| end++; |
| } |
| } |
| } |
| String killed = buf.substring(start, end); |
| buf.cursor(start); |
| buf.delete(end - start); |
| killRing.add(killed); |
| return true; |
| } |
| |
| /** |
| * Kill the buffer ahead of the current cursor position. |
| * |
| * @return true if successful |
| */ |
| public boolean killLine() { |
| if (count < 0) { |
| return callNeg(this::backwardKillLine); |
| } |
| if (buf.cursor() == buf.length()) { |
| return false; |
| } |
| int cp = buf.cursor(); |
| int len = cp; |
| while (count-- > 0) { |
| if (buf.atChar(len) == '\n') { |
| len++; |
| } else { |
| while (buf.atChar(len) != 0 && buf.atChar(len) != '\n') { |
| len++; |
| } |
| } |
| } |
| int num = len - cp; |
| String killed = buf.substring(cp, cp + num); |
| buf.delete(num); |
| killRing.add(killed); |
| return true; |
| } |
| |
| public boolean backwardKillLine() { |
| if (count < 0) { |
| return callNeg(this::killLine); |
| } |
| if (buf.cursor() == 0) { |
| return false; |
| } |
| int cp = buf.cursor(); |
| int beg = cp; |
| while (count-- > 0) { |
| if (beg == 0) { |
| break; |
| } |
| if (buf.atChar(beg - 1) == '\n') { |
| beg--; |
| } else { |
| while (beg > 0 && buf.atChar(beg - 1) != 0 && buf.atChar(beg - 1) != '\n') { |
| beg--; |
| } |
| } |
| } |
| int num = cp - beg; |
| String killed = buf.substring(cp - beg, cp); |
| buf.cursor(beg); |
| buf.delete(num); |
| killRing.add(killed); |
| return true; |
| } |
| |
| public boolean killRegion() { |
| return doCopyKillRegion(true); |
| } |
| |
| public boolean copyRegionAsKill() { |
| return doCopyKillRegion(false); |
| } |
| |
| private boolean doCopyKillRegion(boolean kill) { |
| if (regionMark > buf.length()) { |
| regionMark = buf.length(); |
| } |
| if (regionActive == RegionType.LINE) { |
| int start = regionMark; |
| int end = buf.cursor(); |
| if (start < end) { |
| while (start > 0 && buf.atChar(start - 1) != '\n') { |
| start--; |
| } |
| while (end < buf.length() - 1 && buf.atChar(end + 1) != '\n') { |
| end++; |
| } |
| if (isInViCmdMode()) { |
| end++; |
| } |
| killRing.add(buf.substring(start, end)); |
| if (kill) { |
| buf.backspace(end - start); |
| } |
| } else { |
| while (end > 0 && buf.atChar(end - 1) != '\n') { |
| end--; |
| } |
| while (start < buf.length() && buf.atChar(start) != '\n') { |
| start++; |
| } |
| if (isInViCmdMode()) { |
| start++; |
| } |
| killRing.addBackwards(buf.substring(end, start)); |
| if (kill) { |
| buf.cursor(end); |
| buf.delete(start - end); |
| } |
| } |
| } else if (regionMark > buf.cursor()) { |
| if (isInViCmdMode()) { |
| regionMark++; |
| } |
| killRing.add(buf.substring(buf.cursor(), regionMark)); |
| if (kill) { |
| buf.delete(regionMark - buf.cursor()); |
| } |
| } else { |
| if (isInViCmdMode()) { |
| buf.move(1); |
| } |
| killRing.add(buf.substring(regionMark, buf.cursor())); |
| if (kill) { |
| buf.backspace(buf.cursor() - regionMark); |
| } |
| } |
| if (kill) { |
| regionActive = RegionType.NONE; |
| } |
| return true; |
| } |
| |
| public boolean yank() { |
| String yanked = killRing.yank(); |
| if (yanked == null) { |
| return false; |
| } else { |
| putString(yanked); |
| return true; |
| } |
| } |
| |
| public boolean yankPop() { |
| if (!killRing.lastYank()) { |
| return false; |
| } |
| String current = killRing.yank(); |
| if (current == null) { |
| // This shouldn't happen. |
| return false; |
| } |
| buf.backspace(current.length()); |
| String yanked = killRing.yankPop(); |
| if (yanked == null) { |
| // This shouldn't happen. |
| return false; |
| } |
| |
| putString(yanked); |
| return true; |
| } |
| |
| public boolean mouse() { |
| MouseEvent event = readMouseEvent(); |
| if (event.getType() == MouseEvent.Type.Released |
| && event.getButton() == MouseEvent.Button.Button1) { |
| StringBuilder tsb = new StringBuilder(); |
| Cursor cursor = terminal.getCursorPosition(c -> tsb.append((char) c)); |
| bindingReader.runMacro(tsb.toString()); |
| |
| List<AttributedString> secondaryPrompts = new ArrayList<>(); |
| getDisplayedBufferWithPrompts(secondaryPrompts); |
| |
| AttributedStringBuilder sb = new AttributedStringBuilder().tabs(TAB_WIDTH); |
| sb.append(prompt); |
| sb.append(insertSecondaryPrompts(new AttributedString(buf.upToCursor()), secondaryPrompts, false)); |
| List<AttributedString> promptLines = sb.columnSplitLength(size.getColumns(), false, display.delayLineWrap()); |
| |
| int currentLine = promptLines.size() - 1; |
| int wantedLine = Math.max(0, Math.min(currentLine + event.getY() - cursor.getY(), secondaryPrompts.size())); |
| int pl0 = currentLine == 0 ? prompt.columnLength() : secondaryPrompts.get(currentLine - 1).columnLength(); |
| int pl1 = wantedLine == 0 ? prompt.columnLength() : secondaryPrompts.get(wantedLine - 1).columnLength(); |
| int adjust = pl1 - pl0; |
| buf.moveXY(event.getX() - cursor.getX() - adjust, event.getY() - cursor.getY()); |
| } |
| return true; |
| } |
| |
| public boolean beginPaste() { |
| String str = doReadStringUntil(BRACKETED_PASTE_END); |
| regionActive = RegionType.PASTE; |
| regionMark = getBuffer().cursor(); |
| getBuffer().write(str.replace('\r', '\n')); |
| return true; |
| } |
| |
| public boolean focusIn() { |
| return false; |
| } |
| |
| public boolean focusOut() { |
| return false; |
| } |
| |
| /** |
| * Clean the used display |
| * @return <code>true</code> |
| */ |
| public boolean clear() { |
| display.update(Collections.emptyList(), 0); |
| return true; |
| } |
| |
| /** |
| * Clear the screen by issuing the ANSI "clear screen" code. |
| * @return <code>true</code> |
| */ |
| 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(); |
| } |
| redrawLine(); |
| } else { |
| println(); |
| } |
| return true; |
| } |
| |
| /** |
| * Issue an audible keyboard bell. |
| * @return <code>true</code> |
| */ |
| public boolean beep() { |
| BellType bell_preference = BellType.AUDIBLE; |
| switch (getString(BELL_STYLE, DEFAULT_BELL_STYLE).toLowerCase()) { |
| case "none": |
| case "off": |
| bell_preference = BellType.NONE; |
| break; |
| case "audible": |
| bell_preference = BellType.AUDIBLE; |
| break; |
| case "visible": |
| bell_preference = BellType.VISIBLE; |
| break; |
| case "on": |
| bell_preference = getBoolean(PREFER_VISIBLE_BELL, false) |
| ? BellType.VISIBLE : BellType.AUDIBLE; |
| break; |
| } |
| if (bell_preference == BellType.VISIBLE) { |
| if (terminal.puts(Capability.flash_screen) |
| || terminal.puts(Capability.bell)) { |
| flush(); |
| } |
| } else if (bell_preference == BellType.AUDIBLE) { |
| if (terminal.puts(Capability.bell)) { |
| flush(); |
| } |
| } |
| return true; |
| } |
| |
| // |
| // 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 |
| */ |
| protected boolean isDelimiter(int 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 |
| */ |
| protected boolean isWhitespace(int c) { |
| return Character.isWhitespace(c); |
| } |
| |
| protected boolean isViAlphaNum(int c) { |
| return c == '_' || Character.isLetterOrDigit(c); |
| } |
| |
| protected boolean isAlpha(int c) { |
| return Character.isLetter(c); |
| } |
| |
| protected boolean isWord(int c) { |
| String wordchars = getString(WORDCHARS, DEFAULT_WORDCHARS); |
| return Character.isLetterOrDigit(c) |
| || (c < 128 && wordchars.indexOf((char) c) >= 0); |
| } |
| |
| String getString(String name, String def) { |
| return ReaderUtils.getString(this, name, def); |
| } |
| |
| boolean getBoolean(String name, boolean def) { |
| return ReaderUtils.getBoolean(this, name, def); |
| } |
| |
| int getInt(String name, int def) { |
| return ReaderUtils.getInt(this, name, def); |
| } |
| |
| long getLong(String name, long def) { |
| return ReaderUtils.getLong(this, name, def); |
| } |
| |
| @Override |
| public Map<String, KeyMap<Binding>> defaultKeyMaps() { |
| Map<String, KeyMap<Binding>> keyMaps = new HashMap<>(); |
| keyMaps.put(EMACS, emacs()); |
| keyMaps.put(VICMD, viCmd()); |
| keyMaps.put(VIINS, viInsertion()); |
| keyMaps.put(MENU, menu()); |
| keyMaps.put(VIOPP, viOpp()); |
| keyMaps.put(VISUAL, visual()); |
| keyMaps.put(SAFE, safe()); |
| if (getBoolean(BIND_TTY_SPECIAL_CHARS, true)) { |
| Attributes attr = terminal.getAttributes(); |
| bindConsoleChars(keyMaps.get(EMACS), attr); |
| bindConsoleChars(keyMaps.get(VIINS), attr); |
| } |
| // Put default |
| for (KeyMap<Binding> keyMap : keyMaps.values()) { |
| keyMap.setUnicode(new Reference(SELF_INSERT)); |
| keyMap.setAmbiguousTimeout(getLong(AMBIGUOUS_BINDING, DEFAULT_AMBIGUOUS_BINDING)); |
| } |
| // By default, link main to emacs |
| keyMaps.put(MAIN, keyMaps.get(EMACS)); |
| return keyMaps; |
| } |
| |
| 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')); |
| bind(emacs, DELETE_CHAR_OR_LIST, ctrl('D')); |
| bind(emacs, END_OF_LINE, ctrl('E')); |
| bind(emacs, FORWARD_CHAR, ctrl('F')); |
| bind(emacs, SEND_BREAK, ctrl('G')); |
| bind(emacs, BACKWARD_DELETE_CHAR, ctrl('H')); |
| bind(emacs, EXPAND_OR_COMPLETE, ctrl('I')); |
| bind(emacs, ACCEPT_LINE, ctrl('J')); |
| bind(emacs, KILL_LINE, ctrl('K')); |
| 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')); |
| bind(emacs, TRANSPOSE_CHARS, ctrl('T')); |
| bind(emacs, KILL_WHOLE_LINE, ctrl('U')); |
| bind(emacs, QUOTED_INSERT, ctrl('V')); |
| bind(emacs, BACKWARD_KILL_WORD, ctrl('W')); |
| bind(emacs, YANK, ctrl('Y')); |
| bind(emacs, CHARACTER_SEARCH, ctrl(']')); |
| bind(emacs, UNDO, ctrl('_')); |
| bind(emacs, SELF_INSERT, range(" -~")); |
| bind(emacs, INSERT_CLOSE_PAREN, ")"); |
| bind(emacs, INSERT_CLOSE_SQUARE, "]"); |
| bind(emacs, INSERT_CLOSE_CURLY, "}"); |
| bind(emacs, BACKWARD_DELETE_CHAR, del()); |
| bind(emacs, VI_MATCH_BRACKET, translate("^X^B")); |
| bind(emacs, SEND_BREAK, translate("^X^G")); |
| bind(emacs, EDIT_AND_EXECUTE_COMMAND, translate("^X^E")); |
| bind(emacs, VI_FIND_NEXT_CHAR, translate("^X^F")); |
| bind(emacs, VI_JOIN, translate("^X^J")); |
| bind(emacs, KILL_BUFFER, translate("^X^K")); |
| bind(emacs, INFER_NEXT_HISTORY, translate("^X^N")); |
| bind(emacs, OVERWRITE_MODE, translate("^X^O")); |
| bind(emacs, REDO, translate("^X^R")); |
| bind(emacs, UNDO, translate("^X^U")); |
| bind(emacs, VI_CMD_MODE, translate("^X^V")); |
| bind(emacs, EXCHANGE_POINT_AND_MARK, translate("^X^X")); |
| bind(emacs, DO_LOWERCASE_VERSION, translate("^XA-^XZ")); |
| bind(emacs, WHAT_CURSOR_POSITION, translate("^X=")); |
| bind(emacs, KILL_LINE, translate("^X^?")); |
| bind(emacs, SEND_BREAK, alt(ctrl('G'))); |
| bind(emacs, BACKWARD_KILL_WORD, alt(ctrl('H'))); |
| bind(emacs, SELF_INSERT_UNMETA, alt(ctrl('M'))); |
| bind(emacs, COMPLETE_WORD, alt(esc())); |
| bind(emacs, CHARACTER_SEARCH_BACKWARD, alt(ctrl(']'))); |
| bind(emacs, COPY_PREV_WORD, alt(ctrl('_'))); |
| bind(emacs, SET_MARK_COMMAND, alt(' ')); |
| bind(emacs, NEG_ARGUMENT, alt('-')); |
| bind(emacs, DIGIT_ARGUMENT, range("\\E0-\\E9")); |
| bind(emacs, BEGINNING_OF_HISTORY, alt('<')); |
| bind(emacs, LIST_CHOICES, alt('=')); |
| 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')); |
| bind(emacs, KILL_WORD, translate("^[[3;5~")); // ctrl-delete |
| bind(emacs, FORWARD_WORD, alt('f')); |
| bind(emacs, DOWN_CASE_WORD, alt('l')); |
| bind(emacs, HISTORY_SEARCH_FORWARD, alt('n')); |
| bind(emacs, HISTORY_SEARCH_BACKWARD, alt('p')); |
| bind(emacs, TRANSPOSE_WORDS, alt('t')); |
| bind(emacs, UP_CASE_WORD, alt('u')); |
| bind(emacs, YANK_POP, alt('y')); |
| bind(emacs, BACKWARD_KILL_WORD, alt(del())); |
| bindArrowKeys(emacs); |
| bind(emacs, FORWARD_WORD, translate("^[[1;5C")); // ctrl-left |
| bind(emacs, BACKWARD_WORD, translate("^[[1;5D")); // ctrl-right |
| bind(emacs, FORWARD_WORD, alt(key(Capability.key_right))); |
| bind(emacs, BACKWARD_WORD, alt(key(Capability.key_left))); |
| bind(emacs, FORWARD_WORD, alt(translate("^[[C"))); |
| bind(emacs, BACKWARD_WORD, alt(translate("^[[D"))); |
| return emacs; |
| } |
| |
| 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')); |
| bind(viins, BACKWARD_DELETE_CHAR, ctrl('H')); |
| bind(viins, EXPAND_OR_COMPLETE, ctrl('I')); |
| bind(viins, ACCEPT_LINE, ctrl('J')); |
| bind(viins, CLEAR_SCREEN, ctrl('L')); |
| bind(viins, ACCEPT_LINE, ctrl('M')); |
| bind(viins, MENU_COMPLETE, ctrl('N')); |
| bind(viins, REVERSE_MENU_COMPLETE, ctrl('P')); |
| bind(viins, HISTORY_INCREMENTAL_SEARCH_BACKWARD, ctrl('R')); |
| bind(viins, HISTORY_INCREMENTAL_SEARCH_FORWARD, ctrl('S')); |
| bind(viins, TRANSPOSE_CHARS, ctrl('T')); |
| bind(viins, KILL_WHOLE_LINE, ctrl('U')); |
| bind(viins, QUOTED_INSERT, ctrl('V')); |
| bind(viins, BACKWARD_KILL_WORD, ctrl('W')); |
| bind(viins, YANK, ctrl('Y')); |
| bind(viins, VI_CMD_MODE, ctrl('[')); |
| bind(viins, UNDO, ctrl('_')); |
| bind(viins, HISTORY_INCREMENTAL_SEARCH_BACKWARD, ctrl('X') + "r"); |
| bind(viins, HISTORY_INCREMENTAL_SEARCH_FORWARD, ctrl('X') + "s"); |
| bind(viins, SELF_INSERT, range(" -~")); |
| bind(viins, INSERT_CLOSE_PAREN, ")"); |
| bind(viins, INSERT_CLOSE_SQUARE, "]"); |
| bind(viins, INSERT_CLOSE_CURLY, "}"); |
| bind(viins, BACKWARD_DELETE_CHAR, del()); |
| bindArrowKeys(viins); |
| return viins; |
| } |
| |
| public KeyMap<Binding> viCmd() { |
| KeyMap<Binding> vicmd = new KeyMap<>(); |
| bind(vicmd, LIST_CHOICES, ctrl('D')); |
| bind(vicmd, EMACS_EDITING_MODE, ctrl('E')); |
| bind(vicmd, SEND_BREAK, ctrl('G')); |
| bind(vicmd, VI_BACKWARD_CHAR, ctrl('H')); |
| bind(vicmd, ACCEPT_LINE, ctrl('J')); |
| bind(vicmd, KILL_LINE, ctrl('K')); |
| bind(vicmd, CLEAR_SCREEN, ctrl('L')); |
| bind(vicmd, ACCEPT_LINE, ctrl('M')); |
| bind(vicmd, VI_DOWN_LINE_OR_HISTORY, ctrl('N')); |
| bind(vicmd, VI_UP_LINE_OR_HISTORY, ctrl('P')); |
| bind(vicmd, QUOTED_INSERT, ctrl('Q')); |
| bind(vicmd, HISTORY_INCREMENTAL_SEARCH_BACKWARD, ctrl('R')); |
| bind(vicmd, HISTORY_INCREMENTAL_SEARCH_FORWARD, ctrl('S')); |
| bind(vicmd, TRANSPOSE_CHARS, ctrl('T')); |
| bind(vicmd, KILL_WHOLE_LINE, ctrl('U')); |
| bind(vicmd, QUOTED_INSERT, ctrl('V')); |
| bind(vicmd, BACKWARD_KILL_WORD, ctrl('W')); |
| bind(vicmd, YANK, ctrl('Y')); |
| bind(vicmd, HISTORY_INCREMENTAL_SEARCH_BACKWARD, ctrl('X') + "r"); |
| bind(vicmd, HISTORY_INCREMENTAL_SEARCH_FORWARD, ctrl('X') + "s"); |
| bind(vicmd, SEND_BREAK, alt(ctrl('G'))); |
| bind(vicmd, BACKWARD_KILL_WORD, alt(ctrl('H'))); |
| bind(vicmd, SELF_INSERT_UNMETA, alt(ctrl('M'))); |
| bind(vicmd, COMPLETE_WORD, alt(esc())); |
| bind(vicmd, CHARACTER_SEARCH_BACKWARD, alt(ctrl(']'))); |
| bind(vicmd, SET_MARK_COMMAND, alt(' ')); |
| // bind(vicmd, INSERT_COMMENT, alt('#')); |
| // bind(vicmd, INSERT_COMPLETIONS, alt('*')); |
| bind(vicmd, DIGIT_ARGUMENT, alt('-')); |
| bind(vicmd, BEGINNING_OF_HISTORY, alt('<')); |
| bind(vicmd, LIST_CHOICES, alt('=')); |
| bind(vicmd, END_OF_HISTORY, alt('>')); |
| bind(vicmd, LIST_CHOICES, alt('?')); |
| bind(vicmd, DO_LOWERCASE_VERSION, range("^[A-^[Z")); |
| bind(vicmd, BACKWARD_WORD, alt('b')); |
| bind(vicmd, CAPITALIZE_WORD, alt('c')); |
| bind(vicmd, KILL_WORD, alt('d')); |
| bind(vicmd, FORWARD_WORD, alt('f')); |
| bind(vicmd, DOWN_CASE_WORD, alt('l')); |
| bind(vicmd, HISTORY_SEARCH_FORWARD, alt('n')); |
| bind(vicmd, HISTORY_SEARCH_BACKWARD, alt('p')); |
| bind(vicmd, TRANSPOSE_WORDS, alt('t')); |
| bind(vicmd, UP_CASE_WORD, alt('u')); |
| bind(vicmd, YANK_POP, alt('y')); |
| bind(vicmd, BACKWARD_KILL_WORD, alt(del())); |
| |
| bind(vicmd, FORWARD_CHAR, " "); |
| bind(vicmd, VI_INSERT_COMMENT, "#"); |
| bind(vicmd, END_OF_LINE, "$"); |
| bind(vicmd, VI_MATCH_BRACKET, "%"); |
| bind(vicmd, VI_DOWN_LINE_OR_HISTORY, "+"); |
| bind(vicmd, VI_REV_REPEAT_FIND, ","); |
| bind(vicmd, VI_UP_LINE_OR_HISTORY, "-"); |
| bind(vicmd, VI_REPEAT_CHANGE, "."); |
| bind(vicmd, VI_HISTORY_SEARCH_BACKWARD, "/"); |
| bind(vicmd, VI_DIGIT_OR_BEGINNING_OF_LINE, "0"); |
| bind(vicmd, DIGIT_ARGUMENT, range("1-9")); |
| bind(vicmd, VI_REPEAT_FIND, ";"); |
| bind(vicmd, LIST_CHOICES, "="); |
| bind(vicmd, VI_HISTORY_SEARCH_FORWARD, "?"); |
| bind(vicmd, VI_ADD_EOL, "A"); |
| bind(vicmd, VI_BACKWARD_BLANK_WORD, "B"); |
| bind(vicmd, VI_CHANGE_EOL, "C"); |
| bind(vicmd, VI_KILL_EOL, "D"); |
| bind(vicmd, VI_FORWARD_BLANK_WORD_END, "E"); |
| bind(vicmd, VI_FIND_PREV_CHAR, "F"); |
| bind(vicmd, VI_FETCH_HISTORY, "G"); |
| bind(vicmd, VI_INSERT_BOL, "I"); |
| bind(vicmd, VI_JOIN, "J"); |
| bind(vicmd, VI_REV_REPEAT_SEARCH, "N"); |
| bind(vicmd, VI_OPEN_LINE_ABOVE, "O"); |
| bind(vicmd, VI_PUT_BEFORE, "P"); |
| bind(vicmd, VI_REPLACE, "R"); |
| bind(vicmd, VI_KILL_LINE, "S"); |
| bind(vicmd, VI_FIND_PREV_CHAR_SKIP, "T"); |
| bind(vicmd, REDO, "U"); |
| bind(vicmd, VISUAL_LINE_MODE, "V"); |
| bind(vicmd, VI_FORWARD_BLANK_WORD, "W"); |
| bind(vicmd, VI_BACKWARD_DELETE_CHAR, "X"); |
| bind(vicmd, VI_YANK_WHOLE_LINE, "Y"); |
| bind(vicmd, VI_FIRST_NON_BLANK, "^"); |
| bind(vicmd, VI_ADD_NEXT, "a"); |
| bind(vicmd, VI_BACKWARD_WORD, "b"); |
| bind(vicmd, VI_CHANGE, "c"); |
| bind(vicmd, VI_DELETE, "d"); |
| bind(vicmd, VI_FORWARD_WORD_END, "e"); |
| bind(vicmd, VI_FIND_NEXT_CHAR, "f"); |
| bind(vicmd, WHAT_CURSOR_POSITION, "ga"); |
| bind(vicmd, VI_BACKWARD_BLANK_WORD_END, "gE"); |
| bind(vicmd, VI_BACKWARD_WORD_END, "ge"); |
| bind(vicmd, VI_BACKWARD_CHAR, "h"); |
| bind(vicmd, VI_INSERT, "i"); |
| bind(vicmd, DOWN_LINE_OR_HISTORY, "j"); |
| bind(vicmd, UP_LINE_OR_HISTORY, "k"); |
| bind(vicmd, VI_FORWARD_CHAR, "l"); |
| bind(vicmd, VI_REPEAT_SEARCH, "n"); |
| bind(vicmd, VI_OPEN_LINE_BELOW, "o"); |
| bind(vicmd, VI_PUT_AFTER, "p"); |
| bind(vicmd, VI_REPLACE_CHARS, "r"); |
| bind(vicmd, VI_SUBSTITUTE, "s"); |
| bind(vicmd, VI_FIND_NEXT_CHAR_SKIP, "t"); |
| bind(vicmd, UNDO, "u"); |
| bind(vicmd, VISUAL_MODE, "v"); |
| bind(vicmd, VI_FORWARD_WORD, "w"); |
| bind(vicmd, VI_DELETE_CHAR, "x"); |
| bind(vicmd, VI_YANK, "y"); |
| bind(vicmd, VI_GOTO_COLUMN, "|"); |
| bind(vicmd, VI_SWAP_CASE, "~"); |
| bind(vicmd, VI_BACKWARD_CHAR, del()); |
| |
| bindArrowKeys(vicmd); |
| return vicmd; |
| } |
| |
| public KeyMap<Binding> menu() { |
| KeyMap<Binding> menu = new KeyMap<>(); |
| bind(menu, MENU_COMPLETE, "\t"); |
| bind(menu, REVERSE_MENU_COMPLETE, key(Capability.back_tab)); |
| bind(menu, ACCEPT_LINE, "\r", "\n"); |
| bindArrowKeys(menu); |
| return menu; |
| } |
| |
| public KeyMap<Binding> safe() { |
| KeyMap<Binding> safe = new KeyMap<>(); |
| bind(safe, SELF_INSERT, range("^@-^?")); |
| bind(safe, ACCEPT_LINE, "\r", "\n"); |
| bind(safe, SEND_BREAK, ctrl('G')); |
| return safe; |
| } |
| |
| public KeyMap<Binding> visual() { |
| KeyMap<Binding> visual = new KeyMap<>(); |
| bind(visual, UP_LINE, key(Capability.key_up), "k"); |
| bind(visual, DOWN_LINE, key(Capability.key_down), "j"); |
| bind(visual, this::deactivateRegion, esc()); |
| bind(visual, EXCHANGE_POINT_AND_MARK, "o"); |
| bind(visual, PUT_REPLACE_SELECTION, "p"); |
| bind(visual, VI_DELETE, "x"); |
| bind(visual, VI_OPER_SWAP_CASE, "~"); |
| return visual; |
| } |
| |
| public KeyMap<Binding> viOpp() { |
| KeyMap<Binding> viOpp = new KeyMap<>(); |
| bind(viOpp, UP_LINE, key(Capability.key_up), "k"); |
| bind(viOpp, DOWN_LINE, key(Capability.key_down), "j"); |
| bind(viOpp, VI_CMD_MODE, esc()); |
| return viOpp; |
| } |
| |
| private void bind(KeyMap<Binding> map, String widget, Iterable<? extends CharSequence> keySeqs) { |
| map.bind(new Reference(widget), keySeqs); |
| } |
| |
| private void bind(KeyMap<Binding> map, String widget, CharSequence... keySeqs) { |
| map.bind(new Reference(widget), keySeqs); |
| } |
| |
| private void bind(KeyMap<Binding> map, Widget widget, CharSequence... keySeqs) { |
| map.bind(widget, keySeqs); |
| } |
| |
| private String key(Capability capability) { |
| 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)); |
| bind(map, BACKWARD_CHAR, key(Capability.key_left)); |
| bind(map, FORWARD_CHAR, key(Capability.key_right)); |
| bind(map, BEGINNING_OF_LINE, key(Capability.key_home)); |
| bind(map, END_OF_LINE, key(Capability.key_end)); |
| bind(map, DELETE_CHAR, key(Capability.key_dc)); |
| bind(map, KILL_WHOLE_LINE, key(Capability.key_dl)); |
| bind(map, OVERWRITE_MODE, key(Capability.key_ic)); |
| bind(map, MOUSE, key(Capability.key_mouse)); |
| bind(map, BEGIN_PASTE, BRACKETED_PASTE_BEGIN); |
| bind(map, FOCUS_IN, FOCUS_IN_SEQ); |
| bind(map, FOCUS_OUT, FOCUS_OUT_SEQ); |
| } |
| |
| /** |
| * Bind special chars defined by the terminal instead of |
| * the default bindings |
| */ |
| private void bindConsoleChars(KeyMap<Binding> keyMap, Attributes attr) { |
| if (attr != null) { |
| rebind(keyMap, BACKWARD_DELETE_CHAR, |
| del(), (char) attr.getControlChar(ControlChar.VERASE)); |
| rebind(keyMap, BACKWARD_KILL_WORD, |
| ctrl('W'), (char) attr.getControlChar(ControlChar.VWERASE)); |
| rebind(keyMap, KILL_WHOLE_LINE, |
| ctrl('U'), (char) attr.getControlChar(ControlChar.VKILL)); |
| rebind(keyMap, QUOTED_INSERT, |
| ctrl('V'), (char) attr.getControlChar(ControlChar.VLNEXT)); |
| } |
| } |
| |
| private void rebind(KeyMap<Binding> keyMap, String operation, String prevBinding, char newBinding) { |
| if (newBinding > 0 && newBinding < 128) { |
| Reference ref = new Reference(operation); |
| bind(keyMap, SELF_INSERT, prevBinding); |
| keyMap.bind(ref, Character.toString(newBinding)); |
| } |
| } |
| |
| } |