1 /* 2 * Copyright (c) 2002-2012, the original author or authors. 3 * 4 * This software is distributable under the BSD license. See the terms of the 5 * BSD license in the documentation provided with this software. 6 * 7 * http://www.opensource.org/licenses/bsd-license.php 8 */ 9 package jdk.internal.jline.console; 10 11 //import java.awt.*; 12 //import java.awt.datatransfer.Clipboard; 13 //import java.awt.datatransfer.DataFlavor; 14 //import java.awt.datatransfer.Transferable; 15 //import java.awt.datatransfer.UnsupportedFlavorException; 16 //import java.awt.event.ActionListener; 17 //import java.io.BufferedReader; 18 import java.io.ByteArrayInputStream; 19 //import java.io.ByteArrayOutputStream; 20 import java.io.File; 21 import java.io.FileDescriptor; 22 import java.io.FileInputStream; 23 import java.io.IOException; 24 import java.io.InputStream; 25 import java.io.OutputStream; 26 import java.io.OutputStreamWriter; 27 import java.io.Reader; 28 import java.io.Writer; 29 import java.lang.reflect.InvocationTargetException; 30 import java.lang.reflect.Method; 31 import java.net.URL; 32 import java.util.Arrays; 33 import java.util.Collection; 34 import java.util.Collections; 35 //import java.util.HashMap; 36 import java.util.LinkedList; 37 import java.util.List; 38 import java.util.ListIterator; 39 //import java.util.Map; 40 import java.util.ResourceBundle; 41 import java.util.Stack; 42 import java.util.regex.Pattern; 43 44 import jdk.internal.jline.Terminal; 45 import jdk.internal.jline.TerminalFactory; 46 import jdk.internal.jline.UnixTerminal; 47 import jdk.internal.jline.console.completer.CandidateListCompletionHandler; 48 import jdk.internal.jline.console.completer.Completer; 49 import jdk.internal.jline.console.completer.CompletionHandler; 50 import jdk.internal.jline.console.history.History; 51 import jdk.internal.jline.console.history.MemoryHistory; 52 import jdk.internal.jline.internal.Configuration; 53 import jdk.internal.jline.internal.InputStreamReader; 54 import jdk.internal.jline.internal.Log; 55 import jdk.internal.jline.internal.NonBlockingInputStream; 56 import jdk.internal.jline.internal.Nullable; 57 import jdk.internal.jline.internal.Urls; 58 //import org.fusesource.jansi.AnsiOutputStream; 59 60 import static jdk.internal.jline.internal.Preconditions.checkNotNull; 61 62 /** 63 * A reader for console applications. It supports custom tab-completion, 64 * saveable command history, and command line editing. On some platforms, 65 * platform-specific commands will need to be issued before the reader will 66 * function properly. See {@link jline.Terminal#init} for convenience 67 * methods for issuing platform-specific setup commands. 68 * 69 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> 70 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a> 71 * @author <a href="mailto:gnodet@gmail.com">Guillaume Nodet</a> 72 */ 73 public class ConsoleReader 74 { 75 public static final String JLINE_NOBELL = "jline.nobell"; 76 77 public static final String JLINE_ESC_TIMEOUT = "jline.esc.timeout"; 78 79 public static final String JLINE_INPUTRC = "jline.inputrc"; 80 81 public static final String INPUT_RC = ".inputrc"; 82 83 public static final String DEFAULT_INPUT_RC = "/etc/inputrc"; 84 85 public static final char BACKSPACE = '\b'; 86 87 public static final char RESET_LINE = '\r'; 88 89 public static final char KEYBOARD_BELL = '\07'; 90 91 public static final char NULL_MASK = 0; 92 93 public static final int TAB_WIDTH = 4; 94 95 private static final ResourceBundle 96 resources = ResourceBundle.getBundle(CandidateListCompletionHandler.class.getName()); 97 98 private final Terminal terminal; 99 100 private final Writer out; 101 102 private final CursorBuffer buf = new CursorBuffer(); 103 104 private String prompt; 105 private int promptLen; 106 107 private boolean expandEvents = true; 108 109 private boolean bellEnabled = !Configuration.getBoolean(JLINE_NOBELL, true); 110 111 private boolean handleUserInterrupt = false; 112 113 private Character mask; 114 115 private Character echoCharacter; 116 117 private StringBuffer searchTerm = null; 118 119 private String previousSearchTerm = ""; 120 121 private int searchIndex = -1; 122 123 private int parenBlinkTimeout = 500; 124 125 /* 126 * The reader and the nonBlockingInput go hand-in-hand. The reader wraps 127 * the nonBlockingInput, but we have to retain a handle to it so that 128 * we can shut down its blocking read thread when we go away. 129 */ 130 private NonBlockingInputStream in; 131 private long escapeTimeout; 132 private Reader reader; 133 134 /* 135 * TODO: Please read the comments about this in setInput(), but this needs 136 * to be done away with. 137 */ 138 private boolean isUnitTestInput; 139 140 /** 141 * Last character searched for with a vi character search 142 */ 143 private char charSearchChar = 0; // Character to search for 144 private char charSearchLastInvokeChar = 0; // Most recent invocation key 145 private char charSearchFirstInvokeChar = 0;// First character that invoked 146 147 /** 148 * The vi yank buffer 149 */ 150 private String yankBuffer = ""; 151 152 private KillRing killRing = new KillRing(); 153 154 private String encoding; 155 156 private boolean recording; 157 158 private String macro = ""; 159 160 private String appName; 161 162 private URL inputrcUrl; 163 164 private ConsoleKeys consoleKeys; 165 166 private String commentBegin = null; 167 168 private boolean skipLF = false; 169 170 /** 171 * Set to true if the reader should attempt to detect copy-n-paste. The 172 * effect of this that an attempt is made to detect if tab is quickly 173 * followed by another character, then it is assumed that the tab was 174 * a literal tab as part of a copy-and-paste operation and is inserted as 175 * such. 176 */ 177 private boolean copyPasteDetection = false; 178 179 /* 180 * Current internal state of the line reader 181 */ 182 private State state = State.NORMAL; 183 184 /** 185 * Possible states in which the current readline operation may be in. 186 */ 187 private static enum State { 188 /** 189 * The user is just typing away 190 */ 191 NORMAL, 192 /** 193 * In the middle of a emacs seach 194 */ 195 SEARCH, 196 FORWARD_SEARCH, 197 /** 198 * VI "yank-to" operation ("y" during move mode) 199 */ 200 VI_YANK_TO, 201 /** 202 * VI "delete-to" operation ("d" during move mode) 203 */ 204 VI_DELETE_TO, 205 /** 206 * VI "change-to" operation ("c" during move mode) 207 */ 208 VI_CHANGE_TO 209 } 210 211 public ConsoleReader() throws IOException { 212 this(null, new FileInputStream(FileDescriptor.in), System.out, null); 213 } 214 215 public ConsoleReader(final InputStream in, final OutputStream out) throws IOException { 216 this(null, in, out, null); 217 } 218 219 public ConsoleReader(final InputStream in, final OutputStream out, final Terminal term) throws IOException { 220 this(null, in, out, term); 221 } 222 223 public ConsoleReader(final @Nullable String appName, final InputStream in, final OutputStream out, final @Nullable Terminal term) throws IOException { 224 this(appName, in, out, term, null); 225 } 226 227 public ConsoleReader(final @Nullable String appName, final InputStream in, final OutputStream out, final @Nullable Terminal term, final @Nullable String encoding) 228 throws IOException 229 { 230 this.appName = appName != null ? appName : "JLine"; 231 this.encoding = encoding != null ? encoding : Configuration.getEncoding(); 232 this.terminal = term != null ? term : TerminalFactory.get(); 233 String outEncoding = terminal.getOutputEncoding() != null? terminal.getOutputEncoding() : this.encoding; 234 this.out = new OutputStreamWriter(terminal.wrapOutIfNeeded(out), outEncoding); 235 setInput( in ); 236 237 this.inputrcUrl = getInputRc(); 238 239 consoleKeys = new ConsoleKeys(this.appName, inputrcUrl); 240 } 241 242 private URL getInputRc() throws IOException { 243 String path = Configuration.getString(JLINE_INPUTRC); 244 if (path == null) { 245 File f = new File(Configuration.getUserHome(), INPUT_RC); 246 if (!f.exists()) { 247 f = new File(DEFAULT_INPUT_RC); 248 } 249 return f.toURI().toURL(); 250 } else { 251 return Urls.create(path); 252 } 253 } 254 255 public KeyMap getKeys() { 256 return consoleKeys.getKeys(); 257 } 258 259 void setInput(final InputStream in) throws IOException { 260 this.escapeTimeout = Configuration.getLong(JLINE_ESC_TIMEOUT, 100); 261 /* 262 * This is gross and here is how to fix it. In getCurrentPosition() 263 * and getCurrentAnsiRow(), the logic is disabled when running unit 264 * tests and the fact that it is a unit test is determined by knowing 265 * if the original input stream was a ByteArrayInputStream. So, this 266 * is our test to do this. What SHOULD happen is that the unit 267 * tests should pass in a terminal that is appropriately configured 268 * such that whatever behavior they expect to happen (or not happen) 269 * happens (or doesn't). 270 * 271 * So, TODO, get rid of this and fix the unit tests. 272 */ 273 this.isUnitTestInput = in instanceof ByteArrayInputStream; 274 boolean nonBlockingEnabled = 275 escapeTimeout > 0L 276 && terminal.isSupported() 277 && in != null; 278 279 /* 280 * If we had a non-blocking thread already going, then shut it down 281 * and start a new one. 282 */ 283 if (this.in != null) { 284 this.in.shutdown(); 285 } 286 287 final InputStream wrapped = terminal.wrapInIfNeeded( in ); 288 289 this.in = new NonBlockingInputStream(wrapped, nonBlockingEnabled); 290 this.reader = new InputStreamReader( this.in, encoding ); 291 } 292 293 /** 294 * Shuts the console reader down. This method should be called when you 295 * have completed using the reader as it shuts down and cleans up resources 296 * that would otherwise be "leaked". 297 */ 298 public void shutdown() { 299 if (in != null) { 300 in.shutdown(); 301 } 302 } 303 304 /** 305 * Shuts down the ConsoleReader if the JVM attempts to clean it up. 306 */ 307 @Override 308 protected void finalize() throws Throwable { 309 try { 310 shutdown(); 311 } 312 finally { 313 super.finalize(); 314 } 315 } 316 317 public InputStream getInput() { 318 return in; 319 } 320 321 public Writer getOutput() { 322 return out; 323 } 324 325 public Terminal getTerminal() { 326 return terminal; 327 } 328 329 public CursorBuffer getCursorBuffer() { 330 return buf; 331 } 332 333 public void setExpandEvents(final boolean expand) { 334 this.expandEvents = expand; 335 } 336 337 public boolean getExpandEvents() { 338 return expandEvents; 339 } 340 341 /** 342 * Enables or disables copy and paste detection. The effect of enabling this 343 * this setting is that when a tab is received immediately followed by another 344 * character, the tab will not be treated as a completion, but as a tab literal. 345 * @param onoff true if detection is enabled 346 */ 347 public void setCopyPasteDetection(final boolean onoff) { 348 copyPasteDetection = onoff; 349 } 350 351 /** 352 * @return true if copy and paste detection is enabled. 353 */ 354 public boolean isCopyPasteDetectionEnabled() { 355 return copyPasteDetection; 356 } 357 358 /** 359 * Set whether the console bell is enabled. 360 * 361 * @param enabled true if enabled; false otherwise 362 * @since 2.7 363 */ 364 public void setBellEnabled(boolean enabled) { 365 this.bellEnabled = enabled; 366 } 367 368 /** 369 * Get whether the console bell is enabled 370 * 371 * @return true if enabled; false otherwise 372 * @since 2.7 373 */ 374 public boolean getBellEnabled() { 375 return bellEnabled; 376 } 377 378 /** 379 * Set whether user interrupts (ctrl-C) are handled by having JLine 380 * throw {@link UserInterruptException} from {@link #readLine}. 381 * Otherwise, the JVM will handle {@code SIGINT} as normal, which 382 * usually causes it to exit. The default is {@code false}. 383 * 384 * @since 2.10 385 */ 386 public void setHandleUserInterrupt(boolean enabled) 387 { 388 this.handleUserInterrupt = enabled; 389 } 390 391 /** 392 * Get whether user interrupt handling is enabled 393 * 394 * @return true if enabled; false otherwise 395 * @since 2.10 396 */ 397 public boolean getHandleUserInterrupt() 398 { 399 return handleUserInterrupt; 400 } 401 402 /** 403 * Sets the string that will be used to start a comment when the 404 * insert-comment key is struck. 405 * @param commentBegin The begin comment string. 406 * @since 2.7 407 */ 408 public void setCommentBegin(String commentBegin) { 409 this.commentBegin = commentBegin; 410 } 411 412 /** 413 * @return the string that will be used to start a comment when the 414 * insert-comment key is struck. 415 * @since 2.7 416 */ 417 public String getCommentBegin() { 418 String str = commentBegin; 419 420 if (str == null) { 421 str = consoleKeys.getVariable("comment-begin"); 422 if (str == null) { 423 str = "#"; 424 } 425 } 426 return str; 427 } 428 429 public void setPrompt(final String prompt) { 430 this.prompt = prompt; 431 this.promptLen = ((prompt == null) ? 0 : stripAnsi(lastLine(prompt)).length()); 432 } 433 434 public String getPrompt() { 435 return prompt; 436 } 437 438 /** 439 * Set the echo character. For example, to have "*" entered when a password is typed: 440 * <p/> 441 * <pre> 442 * myConsoleReader.setEchoCharacter(new Character('*')); 443 * </pre> 444 * <p/> 445 * Setting the character to 446 * <p/> 447 * <pre> 448 * null 449 * </pre> 450 * <p/> 451 * will restore normal character echoing. Setting the character to 452 * <p/> 453 * <pre> 454 * new Character(0) 455 * </pre> 456 * <p/> 457 * will cause nothing to be echoed. 458 * 459 * @param c the character to echo to the console in place of the typed character. 460 */ 461 public void setEchoCharacter(final Character c) { 462 this.echoCharacter = c; 463 } 464 465 /** 466 * Returns the echo character. 467 */ 468 public Character getEchoCharacter() { 469 return echoCharacter; 470 } 471 472 /** 473 * Erase the current line. 474 * 475 * @return false if we failed (e.g., the buffer was empty) 476 */ 477 protected final boolean resetLine() throws IOException { 478 if (buf.cursor == 0) { 479 return false; 480 } 481 482 StringBuilder killed = new StringBuilder(); 483 484 while (buf.cursor > 0) { 485 char c = buf.current(); 486 if (c == 0) { 487 break; 488 } 489 490 killed.append(c); 491 backspace(); 492 } 493 494 String copy = killed.reverse().toString(); 495 killRing.addBackwards(copy); 496 497 return true; 498 } 499 500 int getCursorPosition() { 501 // FIXME: does not handle anything but a line with a prompt absolute position 502 return promptLen + buf.cursor; 503 } 504 505 /** 506 * Returns the text after the last '\n'. 507 * prompt is returned if no '\n' characters are present. 508 * null is returned if prompt is null. 509 */ 510 private String lastLine(String str) { 511 if (str == null) return ""; 512 int last = str.lastIndexOf("\n"); 513 514 if (last >= 0) { 515 return str.substring(last + 1, str.length()); 516 } 517 518 return str; 519 } 520 521 String stripAnsi(String str) { 522 if (str == null) return ""; 523 return ANSI_CODE_PATTERN.matcher(str).replaceAll(""); 524 // try { 525 // ByteArrayOutputStream baos = new ByteArrayOutputStream(); 526 // AnsiOutputStream aos = new AnsiOutputStream(baos); 527 // aos.write(str.getBytes()); 528 // aos.flush(); 529 // return baos.toString(); 530 // } catch (IOException e) { 531 // return str; 532 // } 533 } 534 //where: 535 private static final Pattern ANSI_CODE_PATTERN = Pattern.compile("\033\\[[^@-~]*[@-~]"); 536 537 /** 538 * Move the cursor position to the specified absolute index. 539 */ 540 public final boolean setCursorPosition(final int position) throws IOException { 541 if (position == buf.cursor) { 542 return true; 543 } 544 545 return moveCursor(position - buf.cursor) != 0; 546 } 547 548 /** 549 * Set the current buffer's content to the specified {@link String}. The 550 * visual console will be modified to show the current buffer. 551 * 552 * @param buffer the new contents of the buffer. 553 */ 554 private void setBuffer(final String buffer) throws IOException { 555 // don't bother modifying it if it is unchanged 556 if (buffer.equals(buf.buffer.toString())) { 557 return; 558 } 559 560 // obtain the difference between the current buffer and the new one 561 int sameIndex = 0; 562 563 for (int i = 0, l1 = buffer.length(), l2 = buf.buffer.length(); (i < l1) 564 && (i < l2); i++) { 565 if (buffer.charAt(i) == buf.buffer.charAt(i)) { 566 sameIndex++; 567 } 568 else { 569 break; 570 } 571 } 572 573 int diff = buf.cursor - sameIndex; 574 if (diff < 0) { // we can't backspace here so try from the end of the buffer 575 moveToEnd(); 576 diff = buf.buffer.length() - sameIndex; 577 } 578 579 backspace(diff); // go back for the differences 580 killLine(); // clear to the end of the line 581 buf.buffer.setLength(sameIndex); // the new length 582 putString(buffer.substring(sameIndex)); // append the differences 583 } 584 585 private void setBuffer(final CharSequence buffer) throws IOException { 586 setBuffer(String.valueOf(buffer)); 587 } 588 589 private void setBufferKeepPos(final String buffer) throws IOException { 590 int pos = buf.cursor; 591 setBuffer(buffer); 592 setCursorPosition(pos); 593 } 594 595 private void setBufferKeepPos(final CharSequence buffer) throws IOException { 596 setBufferKeepPos(String.valueOf(buffer)); 597 } 598 599 /** 600 * Output put the prompt + the current buffer 601 */ 602 public final void drawLine() throws IOException { 603 String prompt = getPrompt(); 604 if (prompt != null) { 605 print(prompt); 606 } 607 608 print(buf.buffer.toString()); 609 610 if (buf.length() != buf.cursor) { // not at end of line 611 back(buf.length() - buf.cursor - 1); 612 } 613 // force drawBuffer to check for weird wrap (after clear screen) 614 drawBuffer(); 615 } 616 617 /** 618 * Clear the line and redraw it. 619 */ 620 public final void redrawLine() throws IOException { 621 print(RESET_LINE); 622 // flush(); 623 drawLine(); 624 } 625 626 /** 627 * Clear the buffer and add its contents to the history. 628 * 629 * @return the former contents of the buffer. 630 */ 631 final String finishBuffer() throws IOException { // FIXME: Package protected because used by tests 632 String str = buf.buffer.toString(); 633 String historyLine = str; 634 635 if (expandEvents) { 636 try { 637 str = expandEvents(str); 638 // all post-expansion occurrences of '!' must have been escaped, so re-add escape to each 639 historyLine = str.replace("!", "\\!"); 640 // only leading '^' results in expansion, so only re-add escape for that case 641 historyLine = historyLine.replaceAll("^\\^", "\\\\^"); 642 } catch(IllegalArgumentException e) { 643 Log.error("Could not expand event", e); 644 beep(); 645 buf.clear(); 646 str = ""; 647 } 648 } 649 650 // we only add it to the history if the buffer is not empty 651 // and if mask is null, since having a mask typically means 652 // the string was a password. We clear the mask after this call 653 if (str.length() > 0) { 654 if (mask == null && isHistoryEnabled()) { 655 history.add(historyLine); 656 } 657 else { 658 mask = null; 659 } 660 } 661 662 history.moveToEnd(); 663 664 buf.buffer.setLength(0); 665 buf.cursor = 0; 666 667 return str; 668 } 669 670 /** 671 * Expand event designator such as !!, !#, !3, etc... 672 * See http://www.gnu.org/software/bash/manual/html_node/Event-Designators.html 673 */ 674 @SuppressWarnings("fallthrough") 675 protected String expandEvents(String str) throws IOException { 676 StringBuilder sb = new StringBuilder(); 677 for (int i = 0; i < str.length(); i++) { 678 char c = str.charAt(i); 679 switch (c) { 680 case '\\': 681 // any '\!' should be considered an expansion escape, so skip expansion and strip the escape character 682 // a leading '\^' should be considered an expansion escape, so skip expansion and strip the escape character 683 // otherwise, add the escape 684 if (i + 1 < str.length()) { 685 char nextChar = str.charAt(i+1); 686 if (nextChar == '!' || (nextChar == '^' && i == 0)) { 687 c = nextChar; 688 i++; 689 } 690 } 691 sb.append(c); 692 break; 693 case '!': 694 if (i + 1 < str.length()) { 695 c = str.charAt(++i); 696 boolean neg = false; 697 String rep = null; 698 int i1, idx; 699 switch (c) { 700 case '!': 701 if (history.size() == 0) { 702 throw new IllegalArgumentException("!!: event not found"); 703 } 704 rep = history.get(history.index() - 1).toString(); 705 break; 706 case '#': 707 sb.append(sb.toString()); 708 break; 709 case '?': 710 i1 = str.indexOf('?', i + 1); 711 if (i1 < 0) { 712 i1 = str.length(); 713 } 714 String sc = str.substring(i + 1, i1); 715 i = i1; 716 idx = searchBackwards(sc); 717 if (idx < 0) { 718 throw new IllegalArgumentException("!?" + sc + ": event not found"); 719 } else { 720 rep = history.get(idx).toString(); 721 } 722 break; 723 case '$': 724 if (history.size() == 0) { 725 throw new IllegalArgumentException("!$: event not found"); 726 } 727 String previous = history.get(history.index() - 1).toString().trim(); 728 int lastSpace = previous.lastIndexOf(' '); 729 if(lastSpace != -1) { 730 rep = previous.substring(lastSpace+1); 731 } else { 732 rep = previous; 733 } 734 break; 735 case ' ': 736 case '\t': 737 sb.append('!'); 738 sb.append(c); 739 break; 740 case '-': 741 neg = true; 742 i++; 743 // fall through 744 case '0': 745 case '1': 746 case '2': 747 case '3': 748 case '4': 749 case '5': 750 case '6': 751 case '7': 752 case '8': 753 case '9': 754 i1 = i; 755 for (; i < str.length(); i++) { 756 c = str.charAt(i); 757 if (c < '0' || c > '9') { 758 break; 759 } 760 } 761 idx = 0; 762 try { 763 idx = Integer.parseInt(str.substring(i1, i)); 764 } catch (NumberFormatException e) { 765 throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found"); 766 } 767 if (neg) { 768 if (idx > 0 && idx <= history.size()) { 769 rep = (history.get(history.index() - idx)).toString(); 770 } else { 771 throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found"); 772 } 773 } else { 774 if (idx > history.index() - history.size() && idx <= history.index()) { 775 rep = (history.get(idx - 1)).toString(); 776 } else { 777 throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found"); 778 } 779 } 780 break; 781 default: 782 String ss = str.substring(i); 783 i = str.length(); 784 idx = searchBackwards(ss, history.index(), true); 785 if (idx < 0) { 786 throw new IllegalArgumentException("!" + ss + ": event not found"); 787 } else { 788 rep = history.get(idx).toString(); 789 } 790 break; 791 } 792 if (rep != null) { 793 sb.append(rep); 794 } 795 } else { 796 sb.append(c); 797 } 798 break; 799 case '^': 800 if (i == 0) { 801 int i1 = str.indexOf('^', i + 1); 802 int i2 = str.indexOf('^', i1 + 1); 803 if (i2 < 0) { 804 i2 = str.length(); 805 } 806 if (i1 > 0 && i2 > 0) { 807 String s1 = str.substring(i + 1, i1); 808 String s2 = str.substring(i1 + 1, i2); 809 String s = history.get(history.index() - 1).toString().replace(s1, s2); 810 sb.append(s); 811 i = i2 + 1; 812 break; 813 } 814 } 815 sb.append(c); 816 break; 817 default: 818 sb.append(c); 819 break; 820 } 821 } 822 String result = sb.toString(); 823 if (!str.equals(result)) { 824 print(result); 825 println(); 826 flush(); 827 } 828 return result; 829 830 } 831 832 /** 833 * Write out the specified string to the buffer and the output stream. 834 */ 835 public final void putString(final CharSequence str) throws IOException { 836 buf.write(str); 837 if (mask == null) { 838 // no masking 839 print(str); 840 } else if (mask == NULL_MASK) { 841 // don't print anything 842 } else { 843 print(mask, str.length()); 844 } 845 drawBuffer(); 846 } 847 848 /** 849 * Redraw the rest of the buffer from the cursor onwards. This is necessary 850 * for inserting text into the buffer. 851 * 852 * @param clear the number of characters to clear after the end of the buffer 853 */ 854 private void drawBuffer(final int clear) throws IOException { 855 // debug ("drawBuffer: " + clear); 856 if (buf.cursor == buf.length() && clear == 0) { 857 } else { 858 char[] chars = buf.buffer.substring(buf.cursor).toCharArray(); 859 if (mask != null) { 860 Arrays.fill(chars, mask); 861 } 862 if (terminal.hasWeirdWrap()) { 863 // need to determine if wrapping will occur: 864 int width = terminal.getWidth(); 865 int pos = getCursorPosition(); 866 for (int i = 0; i < chars.length; i++) { 867 print(chars[i]); 868 if ((pos + i + 1) % width == 0) { 869 print(32); // move cursor to next line by printing dummy space 870 print(13); // CR / not newline. 871 } 872 } 873 } else { 874 print(chars); 875 } 876 clearAhead(clear, chars.length); 877 if (terminal.isAnsiSupported()) { 878 if (chars.length > 0) { 879 back(chars.length); 880 } 881 } else { 882 back(chars.length); 883 } 884 } 885 if (terminal.hasWeirdWrap()) { 886 int width = terminal.getWidth(); 887 // best guess on whether the cursor is in that weird location... 888 // Need to do this without calling ansi cursor location methods 889 // otherwise it breaks paste of wrapped lines in xterm. 890 if (getCursorPosition() > 0 && (getCursorPosition() % width == 0) 891 && buf.cursor == buf.length() && clear == 0) { 892 // the following workaround is reverse-engineered from looking 893 // at what bash sent to the terminal in the same situation 894 print(32); // move cursor to next line by printing dummy space 895 print(13); // CR / not newline. 896 } 897 } 898 } 899 900 /** 901 * Redraw the rest of the buffer from the cursor onwards. This is necessary 902 * for inserting text into the buffer. 903 */ 904 private void drawBuffer() throws IOException { 905 drawBuffer(0); 906 } 907 908 /** 909 * Clear ahead the specified number of characters without moving the cursor. 910 * 911 * @param num the number of characters to clear 912 * @param delta the difference between the internal cursor and the screen 913 * cursor - if > 0, assume some stuff was printed and weird wrap has to be 914 * checked 915 */ 916 private void clearAhead(final int num, int delta) throws IOException { 917 if (num == 0) { 918 return; 919 } 920 921 if (terminal.isAnsiSupported()) { 922 int width = terminal.getWidth(); 923 int screenCursorCol = getCursorPosition() + delta; 924 // clear current line 925 printAnsiSequence("K"); 926 // if cursor+num wraps, then we need to clear the line(s) below too 927 int curCol = screenCursorCol % width; 928 int endCol = (screenCursorCol + num - 1) % width; 929 int lines = num / width; 930 if (endCol < curCol) lines++; 931 for (int i = 0; i < lines; i++) { 932 printAnsiSequence("B"); 933 printAnsiSequence("2K"); 934 } 935 for (int i = 0; i < lines; i++) { 936 printAnsiSequence("A"); 937 } 938 return; 939 } 940 941 // print blank extra characters 942 print(' ', num); 943 944 // we need to flush here so a "clever" console doesn't just ignore the redundancy 945 // of a space followed by a backspace. 946 // flush(); 947 948 // reset the visual cursor 949 back(num); 950 951 // flush(); 952 } 953 954 /** 955 * Move the visual cursor backwards without modifying the buffer cursor. 956 */ 957 protected void back(final int num) throws IOException { 958 if (num == 0) return; 959 if (terminal.isAnsiSupported()) { 960 int width = getTerminal().getWidth(); 961 int cursor = getCursorPosition(); 962 int realCursor = cursor + num; 963 int realCol = realCursor % width; 964 int newCol = cursor % width; 965 int moveup = num / width; 966 int delta = realCol - newCol; 967 if (delta < 0) moveup++; 968 if (moveup > 0) { 969 printAnsiSequence(moveup + "A"); 970 } 971 printAnsiSequence((1 + newCol) + "G"); 972 return; 973 } 974 print(BACKSPACE, num); 975 // flush(); 976 } 977 978 /** 979 * Flush the console output stream. This is important for printout out single characters (like a backspace or 980 * keyboard) that we want the console to handle immediately. 981 */ 982 public void flush() throws IOException { 983 out.flush(); 984 } 985 986 private int backspaceAll() throws IOException { 987 return backspace(Integer.MAX_VALUE); 988 } 989 990 /** 991 * Issue <em>num</em> backspaces. 992 * 993 * @return the number of characters backed up 994 */ 995 private int backspace(final int num) throws IOException { 996 if (buf.cursor == 0) { 997 return 0; 998 } 999 1000 int count = 0; 1001 1002 int termwidth = getTerminal().getWidth(); 1003 int lines = getCursorPosition() / termwidth; 1004 count = moveCursor(-1 * num) * -1; 1005 buf.buffer.delete(buf.cursor, buf.cursor + count); 1006 if (getCursorPosition() / termwidth != lines) { 1007 if (terminal.isAnsiSupported()) { 1008 // debug("doing backspace redraw: " + getCursorPosition() + " on " + termwidth + ": " + lines); 1009 printAnsiSequence("K"); 1010 // if cursor+num wraps, then we need to clear the line(s) below too 1011 // last char printed is one pos less than cursor so we subtract 1012 // one 1013 /* 1014 // TODO: fixme (does not work - test with reverse search with wrapping line and CTRL-E) 1015 int endCol = (getCursorPosition() + num - 1) % termwidth; 1016 int curCol = getCursorPosition() % termwidth; 1017 if (endCol < curCol) lines++; 1018 for (int i = 1; i < lines; i++) { 1019 printAnsiSequence("B"); 1020 printAnsiSequence("2K"); 1021 } 1022 for (int i = 1; i < lines; i++) { 1023 printAnsiSequence("A"); 1024 } 1025 return count; 1026 */ 1027 } 1028 } 1029 drawBuffer(count); 1030 1031 return count; 1032 } 1033 1034 /** 1035 * Issue a backspace. 1036 * 1037 * @return true if successful 1038 */ 1039 public boolean backspace() throws IOException { 1040 return backspace(1) == 1; 1041 } 1042 1043 protected boolean moveToEnd() throws IOException { 1044 if (buf.cursor == buf.length()) { 1045 return true; 1046 } 1047 return moveCursor(buf.length() - buf.cursor) > 0; 1048 } 1049 1050 /** 1051 * Delete the character at the current position and redraw the remainder of the buffer. 1052 */ 1053 private boolean deleteCurrentCharacter() throws IOException { 1054 if (buf.length() == 0 || buf.cursor == buf.length()) { 1055 return false; 1056 } 1057 1058 buf.buffer.deleteCharAt(buf.cursor); 1059 drawBuffer(1); 1060 return true; 1061 } 1062 1063 /** 1064 * This method is calling while doing a delete-to ("d"), change-to ("c"), 1065 * or yank-to ("y") and it filters out only those movement operations 1066 * that are allowable during those operations. Any operation that isn't 1067 * allow drops you back into movement mode. 1068 * 1069 * @param op The incoming operation to remap 1070 * @return The remaped operation 1071 */ 1072 private Operation viDeleteChangeYankToRemap (Operation op) { 1073 switch (op) { 1074 case VI_EOF_MAYBE: 1075 case ABORT: 1076 case BACKWARD_CHAR: 1077 case FORWARD_CHAR: 1078 case END_OF_LINE: 1079 case VI_MATCH: 1080 case VI_BEGNNING_OF_LINE_OR_ARG_DIGIT: 1081 case VI_ARG_DIGIT: 1082 case VI_PREV_WORD: 1083 case VI_END_WORD: 1084 case VI_CHAR_SEARCH: 1085 case VI_NEXT_WORD: 1086 case VI_FIRST_PRINT: 1087 case VI_GOTO_MARK: 1088 case VI_COLUMN: 1089 case VI_DELETE_TO: 1090 case VI_YANK_TO: 1091 case VI_CHANGE_TO: 1092 return op; 1093 1094 default: 1095 return Operation.VI_MOVEMENT_MODE; 1096 } 1097 } 1098 1099 /** 1100 * Deletes the previous character from the cursor position 1101 * @param count number of times to do it. 1102 * @return true if it was done. 1103 * @throws IOException 1104 */ 1105 private boolean viRubout(int count) throws IOException { 1106 boolean ok = true; 1107 for (int i = 0; ok && i < count; i++) { 1108 ok = backspace(); 1109 } 1110 return ok; 1111 } 1112 1113 /** 1114 * Deletes the character you are sitting on and sucks the rest of 1115 * the line in from the right. 1116 * @param count Number of times to perform the operation. 1117 * @return true if its works, false if it didn't 1118 * @throws IOException 1119 */ 1120 private boolean viDelete(int count) throws IOException { 1121 boolean ok = true; 1122 for (int i = 0; ok && i < count; i++) { 1123 ok = deleteCurrentCharacter(); 1124 } 1125 return ok; 1126 } 1127 1128 /** 1129 * Switches the case of the current character from upper to lower 1130 * or lower to upper as necessary and advances the cursor one 1131 * position to the right. 1132 * @param count The number of times to repeat 1133 * @return true if it completed successfully, false if not all 1134 * case changes could be completed. 1135 * @throws IOException 1136 */ 1137 private boolean viChangeCase(int count) throws IOException { 1138 boolean ok = true; 1139 for (int i = 0; ok && i < count; i++) { 1140 1141 ok = buf.cursor < buf.buffer.length (); 1142 if (ok) { 1143 char ch = buf.buffer.charAt(buf.cursor); 1144 if (Character.isUpperCase(ch)) { 1145 ch = Character.toLowerCase(ch); 1146 } 1147 else if (Character.isLowerCase(ch)) { 1148 ch = Character.toUpperCase(ch); 1149 } 1150 buf.buffer.setCharAt(buf.cursor, ch); 1151 drawBuffer(1); 1152 moveCursor(1); 1153 } 1154 } 1155 return ok; 1156 } 1157 1158 /** 1159 * Implements the vi change character command (in move-mode "r" 1160 * followed by the character to change to). 1161 * @param count Number of times to perform the action 1162 * @param c The character to change to 1163 * @return Whether or not there were problems encountered 1164 * @throws IOException 1165 */ 1166 private boolean viChangeChar(int count, int c) throws IOException { 1167 // EOF, ESC, or CTRL-C aborts. 1168 if (c < 0 || c == '\033' || c == '\003') { 1169 return true; 1170 } 1171 1172 boolean ok = true; 1173 for (int i = 0; ok && i < count; i++) { 1174 ok = buf.cursor < buf.buffer.length (); 1175 if (ok) { 1176 buf.buffer.setCharAt(buf.cursor, (char) c); 1177 drawBuffer(1); 1178 if (i < (count-1)) { 1179 moveCursor(1); 1180 } 1181 } 1182 } 1183 return ok; 1184 } 1185 1186 /** 1187 * This is a close facsimile of the actual vi previous word logic. In 1188 * actual vi words are determined by boundaries of identity characterse. 1189 * This logic is a bit more simple and simply looks at white space or 1190 * digits or characters. It should be revised at some point. 1191 * 1192 * @param count number of iterations 1193 * @return true if the move was successful, false otherwise 1194 * @throws IOException 1195 */ 1196 private boolean viPreviousWord(int count) throws IOException { 1197 boolean ok = true; 1198 if (buf.cursor == 0) { 1199 return false; 1200 } 1201 1202 int pos = buf.cursor - 1; 1203 for (int i = 0; pos > 0 && i < count; i++) { 1204 // If we are on white space, then move back. 1205 while (pos > 0 && isWhitespace(buf.buffer.charAt(pos))) { 1206 --pos; 1207 } 1208 1209 while (pos > 0 && !isDelimiter(buf.buffer.charAt(pos-1))) { 1210 --pos; 1211 } 1212 1213 if (pos > 0 && i < (count-1)) { 1214 --pos; 1215 } 1216 } 1217 setCursorPosition(pos); 1218 return ok; 1219 } 1220 1221 /** 1222 * Performs the vi "delete-to" action, deleting characters between a given 1223 * span of the input line. 1224 * @param startPos The start position 1225 * @param endPos The end position. 1226 * @param isChange If true, then the delete is part of a change operationg 1227 * (e.g. "c$" is change-to-end-of line, so we first must delete to end 1228 * of line to start the change 1229 * @return true if it succeeded, false otherwise 1230 * @throws IOException 1231 */ 1232 private boolean viDeleteTo(int startPos, int endPos, boolean isChange) throws IOException { 1233 if (startPos == endPos) { 1234 return true; 1235 } 1236 1237 if (endPos < startPos) { 1238 int tmp = endPos; 1239 endPos = startPos; 1240 startPos = tmp; 1241 } 1242 1243 setCursorPosition(startPos); 1244 buf.cursor = startPos; 1245 buf.buffer.delete(startPos, endPos); 1246 drawBuffer(endPos - startPos); 1247 1248 // If we are doing a delete operation (e.g. "d$") then don't leave the 1249 // cursor dangling off the end. In reality the "isChange" flag is silly 1250 // what is really happening is that if we are in "move-mode" then the 1251 // cursor can't be moved off the end of the line, but in "edit-mode" it 1252 // is ok, but I have no easy way of knowing which mode we are in. 1253 if (! isChange && startPos > 0 && startPos == buf.length()) { 1254 moveCursor(-1); 1255 } 1256 return true; 1257 } 1258 1259 /** 1260 * Implement the "vi" yank-to operation. This operation allows you 1261 * to yank the contents of the current line based upon a move operation, 1262 * for exaple "yw" yanks the current word, "3yw" yanks 3 words, etc. 1263 * 1264 * @param startPos The starting position from which to yank 1265 * @param endPos The ending position to which to yank 1266 * @return true if the yank succeeded 1267 * @throws IOException 1268 */ 1269 private boolean viYankTo(int startPos, int endPos) throws IOException { 1270 int cursorPos = startPos; 1271 1272 if (endPos < startPos) { 1273 int tmp = endPos; 1274 endPos = startPos; 1275 startPos = tmp; 1276 } 1277 1278 if (startPos == endPos) { 1279 yankBuffer = ""; 1280 return true; 1281 } 1282 1283 yankBuffer = buf.buffer.substring(startPos, endPos); 1284 1285 /* 1286 * It was a movement command that moved the cursor to find the 1287 * end position, so put the cursor back where it started. 1288 */ 1289 setCursorPosition(cursorPos); 1290 return true; 1291 } 1292 1293 /** 1294 * Pasts the yank buffer to the right of the current cursor position 1295 * and moves the cursor to the end of the pasted region. 1296 * 1297 * @param count Number of times to perform the operation. 1298 * @return true if it worked, false otherwise 1299 * @throws IOException 1300 */ 1301 private boolean viPut(int count) throws IOException { 1302 if (yankBuffer.length () == 0) { 1303 return true; 1304 } 1305 if (buf.cursor < buf.buffer.length ()) { 1306 moveCursor(1); 1307 } 1308 for (int i = 0; i < count; i++) { 1309 putString(yankBuffer); 1310 } 1311 moveCursor(-1); 1312 return true; 1313 } 1314 1315 /** 1316 * Searches forward of the current position for a character and moves 1317 * the cursor onto it. 1318 * @param count Number of times to repeat the process. 1319 * @param ch The character to search for 1320 * @return true if the char was found, false otherwise 1321 * @throws IOException 1322 */ 1323 private boolean viCharSearch(int count, int invokeChar, int ch) throws IOException { 1324 if (ch < 0 || invokeChar < 0) { 1325 return false; 1326 } 1327 1328 char searchChar = (char)ch; 1329 boolean isForward; 1330 boolean stopBefore; 1331 1332 /* 1333 * The character stuff turns out to be hairy. Here is how it works: 1334 * f - search forward for ch 1335 * F - search backward for ch 1336 * t - search forward for ch, but stop just before the match 1337 * T - search backward for ch, but stop just after the match 1338 * ; - After [fFtT;], repeat the last search, after ',' reverse it 1339 * , - After [fFtT;], reverse the last search, after ',' repeat it 1340 */ 1341 if (invokeChar == ';' || invokeChar == ',') { 1342 // No recent search done? Then bail 1343 if (charSearchChar == 0) { 1344 return false; 1345 } 1346 1347 // Reverse direction if switching between ',' and ';' 1348 if (charSearchLastInvokeChar == ';' || charSearchLastInvokeChar == ',') { 1349 if (charSearchLastInvokeChar != invokeChar) { 1350 charSearchFirstInvokeChar = switchCase(charSearchFirstInvokeChar); 1351 } 1352 } 1353 else { 1354 if (invokeChar == ',') { 1355 charSearchFirstInvokeChar = switchCase(charSearchFirstInvokeChar); 1356 } 1357 } 1358 1359 searchChar = charSearchChar; 1360 } 1361 else { 1362 charSearchChar = searchChar; 1363 charSearchFirstInvokeChar = (char) invokeChar; 1364 } 1365 1366 charSearchLastInvokeChar = (char)invokeChar; 1367 1368 isForward = Character.isLowerCase(charSearchFirstInvokeChar); 1369 stopBefore = (Character.toLowerCase(charSearchFirstInvokeChar) == 't'); 1370 1371 boolean ok = false; 1372 1373 if (isForward) { 1374 while (count-- > 0) { 1375 int pos = buf.cursor + 1; 1376 while (pos < buf.buffer.length()) { 1377 if (buf.buffer.charAt(pos) == searchChar) { 1378 setCursorPosition(pos); 1379 ok = true; 1380 break; 1381 } 1382 ++pos; 1383 } 1384 } 1385 1386 if (ok) { 1387 if (stopBefore) 1388 moveCursor(-1); 1389 1390 /* 1391 * When in yank-to, move-to, del-to state we actually want to 1392 * go to the character after the one we landed on to make sure 1393 * that the character we ended up on is included in the 1394 * operation 1395 */ 1396 if (isInViMoveOperationState()) { 1397 moveCursor(1); 1398 } 1399 } 1400 } 1401 else { 1402 while (count-- > 0) { 1403 int pos = buf.cursor - 1; 1404 while (pos >= 0) { 1405 if (buf.buffer.charAt(pos) == searchChar) { 1406 setCursorPosition(pos); 1407 ok = true; 1408 break; 1409 } 1410 --pos; 1411 } 1412 } 1413 1414 if (ok && stopBefore) 1415 moveCursor(1); 1416 } 1417 1418 return ok; 1419 } 1420 1421 private char switchCase(char ch) { 1422 if (Character.isUpperCase(ch)) { 1423 return Character.toLowerCase(ch); 1424 } 1425 return Character.toUpperCase(ch); 1426 } 1427 1428 /** 1429 * @return true if line reader is in the middle of doing a change-to 1430 * delete-to or yank-to. 1431 */ 1432 private final boolean isInViMoveOperationState() { 1433 return state == State.VI_CHANGE_TO 1434 || state == State.VI_DELETE_TO 1435 || state == State.VI_YANK_TO; 1436 } 1437 1438 /** 1439 * This is a close facsimile of the actual vi next word logic. 1440 * As with viPreviousWord() this probably needs to be improved 1441 * at some point. 1442 * 1443 * @param count number of iterations 1444 * @return true if the move was successful, false otherwise 1445 * @throws IOException 1446 */ 1447 private boolean viNextWord(int count) throws IOException { 1448 int pos = buf.cursor; 1449 int end = buf.buffer.length(); 1450 1451 for (int i = 0; pos < end && i < count; i++) { 1452 // Skip over letter/digits 1453 while (pos < end && !isDelimiter(buf.buffer.charAt(pos))) { 1454 ++pos; 1455 } 1456 1457 /* 1458 * Don't you love special cases? During delete-to and yank-to 1459 * operations the word movement is normal. However, during a 1460 * change-to, the trailing spaces behind the last word are 1461 * left in tact. 1462 */ 1463 if (i < (count-1) || !(state == State.VI_CHANGE_TO)) { 1464 while (pos < end && isDelimiter(buf.buffer.charAt(pos))) { 1465 ++pos; 1466 } 1467 } 1468 } 1469 1470 setCursorPosition(pos); 1471 return true; 1472 } 1473 1474 /** 1475 * Implements a close facsimile of the vi end-of-word movement. 1476 * If the character is on white space, it takes you to the end 1477 * of the next word. If it is on the last character of a word 1478 * it takes you to the next of the next word. Any other character 1479 * of a word, takes you to the end of the current word. 1480 * 1481 * @param count Number of times to repeat the action 1482 * @return true if it worked. 1483 * @throws IOException 1484 */ 1485 private boolean viEndWord(int count) throws IOException { 1486 int pos = buf.cursor; 1487 int end = buf.buffer.length(); 1488 1489 for (int i = 0; pos < end && i < count; i++) { 1490 if (pos < (end-1) 1491 && !isDelimiter(buf.buffer.charAt(pos)) 1492 && isDelimiter(buf.buffer.charAt (pos+1))) { 1493 ++pos; 1494 } 1495 1496 // If we are on white space, then move back. 1497 while (pos < end && isDelimiter(buf.buffer.charAt(pos))) { 1498 ++pos; 1499 } 1500 1501 while (pos < (end-1) && !isDelimiter(buf.buffer.charAt(pos+1))) { 1502 ++pos; 1503 } 1504 } 1505 setCursorPosition(pos); 1506 return true; 1507 } 1508 1509 private boolean previousWord() throws IOException { 1510 while (isDelimiter(buf.current()) && (moveCursor(-1) != 0)) { 1511 // nothing 1512 } 1513 1514 while (!isDelimiter(buf.current()) && (moveCursor(-1) != 0)) { 1515 // nothing 1516 } 1517 1518 return true; 1519 } 1520 1521 private boolean nextWord() throws IOException { 1522 while (isDelimiter(buf.nextChar()) && (moveCursor(1) != 0)) { 1523 // nothing 1524 } 1525 1526 while (!isDelimiter(buf.nextChar()) && (moveCursor(1) != 0)) { 1527 // nothing 1528 } 1529 1530 return true; 1531 } 1532 1533 /** 1534 * Deletes to the beginning of the word that the cursor is sitting on. 1535 * If the cursor is on white-space, it deletes that and to the beginning 1536 * of the word before it. If the user is not on a word or whitespace 1537 * it deletes up to the end of the previous word. 1538 * 1539 * @param count Number of times to perform the operation 1540 * @return true if it worked, false if you tried to delete too many words 1541 * @throws IOException 1542 */ 1543 private boolean unixWordRubout(int count) throws IOException { 1544 boolean success = true; 1545 StringBuilder killed = new StringBuilder(); 1546 1547 for (; count > 0; --count) { 1548 if (buf.cursor == 0) { 1549 success = false; 1550 break; 1551 } 1552 1553 while (isWhitespace(buf.current())) { 1554 char c = buf.current(); 1555 if (c == 0) { 1556 break; 1557 } 1558 1559 killed.append(c); 1560 backspace(); 1561 } 1562 1563 while (!isWhitespace(buf.current())) { 1564 char c = buf.current(); 1565 if (c == 0) { 1566 break; 1567 } 1568 1569 killed.append(c); 1570 backspace(); 1571 } 1572 } 1573 1574 String copy = killed.reverse().toString(); 1575 killRing.addBackwards(copy); 1576 1577 return success; 1578 } 1579 1580 private String insertComment(boolean isViMode) throws IOException { 1581 String comment = this.getCommentBegin (); 1582 setCursorPosition(0); 1583 putString(comment); 1584 if (isViMode) { 1585 consoleKeys.setKeyMap(KeyMap.VI_INSERT); 1586 } 1587 return accept(); 1588 } 1589 1590 /** 1591 * Similar to putString() but allows the string to be repeated a specific 1592 * number of times, allowing easy support of vi digit arguments to a given 1593 * command. The string is placed as the current cursor position. 1594 * 1595 * @param count The count of times to insert the string. 1596 * @param str The string to insert 1597 * @return true if the operation is a success, false otherwise 1598 * @throws IOException 1599 */ 1600 private boolean insert(int count, final CharSequence str) throws IOException { 1601 for (int i = 0; i < count; i++) { 1602 buf.write(str); 1603 if (mask == null) { 1604 // no masking 1605 print(str); 1606 } else if (mask == NULL_MASK) { 1607 // don't print anything 1608 } else { 1609 print(mask, str.length()); 1610 } 1611 } 1612 drawBuffer(); 1613 return true; 1614 } 1615 1616 /** 1617 * Implements vi search ("/" or "?"). 1618 * @throws IOException 1619 */ 1620 @SuppressWarnings("fallthrough") 1621 private int viSearch(char searchChar) throws IOException { 1622 boolean isForward = (searchChar == '/'); 1623 1624 /* 1625 * This is a little gross, I'm sure there is a more appropriate way 1626 * of saving and restoring state. 1627 */ 1628 CursorBuffer origBuffer = buf.copy(); 1629 1630 // Clear the contents of the current line and 1631 setCursorPosition (0); 1632 killLine(); 1633 1634 // Our new "prompt" is the character that got us into search mode. 1635 putString(Character.toString(searchChar)); 1636 flush(); 1637 1638 boolean isAborted = false; 1639 boolean isComplete = false; 1640 1641 /* 1642 * Readline doesn't seem to do any special character map handling 1643 * here, so I think we are safe. 1644 */ 1645 int ch = -1; 1646 while (!isAborted && !isComplete && (ch = readCharacter()) != -1) { 1647 switch (ch) { 1648 case '\033': // ESC 1649 /* 1650 * The ESC behavior doesn't appear to be readline behavior, 1651 * but it is a little tweak of my own. I like it. 1652 */ 1653 isAborted = true; 1654 break; 1655 case '\010': // Backspace 1656 case '\177': // Delete 1657 backspace(); 1658 /* 1659 * Backspacing through the "prompt" aborts the search. 1660 */ 1661 if (buf.cursor == 0) { 1662 isAborted = true; 1663 } 1664 break; 1665 case '\012': // NL 1666 case '\015': // CR 1667 isComplete = true; 1668 break; 1669 default: 1670 putString(Character.toString((char) ch)); 1671 } 1672 1673 flush(); 1674 } 1675 1676 // If we aborted, then put ourself at the end of the original buffer. 1677 if (ch == -1 || isAborted) { 1678 setCursorPosition(0); 1679 killLine(); 1680 putString(origBuffer.buffer); 1681 setCursorPosition(origBuffer.cursor); 1682 return -1; 1683 } 1684 1685 /* 1686 * The first character of the buffer was the search character itself 1687 * so we discard it. 1688 */ 1689 String searchTerm = buf.buffer.substring(1); 1690 int idx = -1; 1691 1692 /* 1693 * The semantics of the history thing is gross when you want to 1694 * explicitly iterate over entries (without an iterator) as size() 1695 * returns the actual number of entries in the list but get() 1696 * doesn't work the way you think. 1697 */ 1698 int end = history.index(); 1699 int start = (end <= history.size()) ? 0 : end - history.size(); 1700 1701 if (isForward) { 1702 for (int i = start; i < end; i++) { 1703 if (history.get(i).toString().contains(searchTerm)) { 1704 idx = i; 1705 break; 1706 } 1707 } 1708 } 1709 else { 1710 for (int i = end-1; i >= start; i--) { 1711 if (history.get(i).toString().contains(searchTerm)) { 1712 idx = i; 1713 break; 1714 } 1715 } 1716 } 1717 1718 /* 1719 * No match? Then restore what we were working on, but make sure 1720 * the cursor is at the beginning of the line. 1721 */ 1722 if (idx == -1) { 1723 setCursorPosition(0); 1724 killLine(); 1725 putString(origBuffer.buffer); 1726 setCursorPosition(0); 1727 return -1; 1728 } 1729 1730 /* 1731 * Show the match. 1732 */ 1733 setCursorPosition(0); 1734 killLine(); 1735 putString(history.get(idx)); 1736 setCursorPosition(0); 1737 flush(); 1738 1739 /* 1740 * While searching really only the "n" and "N" keys are interpreted 1741 * as movement, any other key is treated as if you are editing the 1742 * line with it, so we return it back up to the caller for interpretation. 1743 */ 1744 isComplete = false; 1745 while (!isComplete && (ch = readCharacter()) != -1) { 1746 boolean forward = isForward; 1747 switch (ch) { 1748 case 'p': case 'P': 1749 forward = !isForward; 1750 // Fallthru 1751 case 'n': case 'N': 1752 boolean isMatch = false; 1753 if (forward) { 1754 for (int i = idx+1; !isMatch && i < end; i++) { 1755 if (history.get(i).toString().contains(searchTerm)) { 1756 idx = i; 1757 isMatch = true; 1758 } 1759 } 1760 } 1761 else { 1762 for (int i = idx - 1; !isMatch && i >= start; i--) { 1763 if (history.get(i).toString().contains(searchTerm)) { 1764 idx = i; 1765 isMatch = true; 1766 } 1767 } 1768 } 1769 if (isMatch) { 1770 setCursorPosition(0); 1771 killLine(); 1772 putString(history.get(idx)); 1773 setCursorPosition(0); 1774 } 1775 break; 1776 default: 1777 isComplete = true; 1778 } 1779 flush(); 1780 } 1781 1782 /* 1783 * Complete? 1784 */ 1785 return ch; 1786 } 1787 1788 public void setParenBlinkTimeout(int timeout) { 1789 parenBlinkTimeout = timeout; 1790 } 1791 1792 private void insertClose(String s) throws IOException { 1793 putString(s); 1794 int closePosition = buf.cursor; 1795 1796 moveCursor(-1); 1797 viMatch(); 1798 1799 1800 if (in.isNonBlockingEnabled()) { 1801 in.peek(parenBlinkTimeout); 1802 } 1803 1804 setCursorPosition(closePosition); 1805 } 1806 1807 /** 1808 * Implements vi style bracket matching ("%" command). The matching 1809 * bracket for the current bracket type that you are sitting on is matched. 1810 * The logic works like so: 1811 * @return true if it worked, false if the cursor was not on a bracket 1812 * character or if there was no matching bracket. 1813 * @throws IOException 1814 */ 1815 private boolean viMatch() throws IOException { 1816 int pos = buf.cursor; 1817 1818 if (pos == buf.length()) { 1819 return false; 1820 } 1821 1822 int type = getBracketType(buf.buffer.charAt (pos)); 1823 int move = (type < 0) ? -1 : 1; 1824 int count = 1; 1825 1826 if (type == 0) 1827 return false; 1828 1829 while (count > 0) { 1830 pos += move; 1831 1832 // Fell off the start or end. 1833 if (pos < 0 || pos >= buf.buffer.length ()) { 1834 return false; 1835 } 1836 1837 int curType = getBracketType(buf.buffer.charAt (pos)); 1838 if (curType == type) { 1839 ++count; 1840 } 1841 else if (curType == -type) { 1842 --count; 1843 } 1844 } 1845 1846 /* 1847 * Slight adjustment for delete-to, yank-to, change-to to ensure 1848 * that the matching paren is consumed 1849 */ 1850 if (move > 0 && isInViMoveOperationState()) 1851 ++pos; 1852 1853 setCursorPosition(pos); 1854 return true; 1855 } 1856 1857 /** 1858 * Given a character determines what type of bracket it is (paren, 1859 * square, curly, or none). 1860 * @param ch The character to check 1861 * @return 1 is square, 2 curly, 3 parent, or zero for none. The value 1862 * will be negated if it is the closing form of the bracket. 1863 */ 1864 private int getBracketType (char ch) { 1865 switch (ch) { 1866 case '[': return 1; 1867 case ']': return -1; 1868 case '{': return 2; 1869 case '}': return -2; 1870 case '(': return 3; 1871 case ')': return -3; 1872 default: 1873 return 0; 1874 } 1875 } 1876 1877 private boolean deletePreviousWord() throws IOException { 1878 StringBuilder killed = new StringBuilder(); 1879 char c; 1880 1881 while (isDelimiter((c = buf.current()))) { 1882 if (c == 0) { 1883 break; 1884 } 1885 1886 killed.append(c); 1887 backspace(); 1888 } 1889 1890 while (!isDelimiter((c = buf.current()))) { 1891 if (c == 0) { 1892 break; 1893 } 1894 1895 killed.append(c); 1896 backspace(); 1897 } 1898 1899 String copy = killed.reverse().toString(); 1900 killRing.addBackwards(copy); 1901 return true; 1902 } 1903 1904 private boolean deleteNextWord() throws IOException { 1905 StringBuilder killed = new StringBuilder(); 1906 char c; 1907 1908 while (isDelimiter((c = buf.nextChar()))) { 1909 if (c == 0) { 1910 break; 1911 } 1912 killed.append(c); 1913 delete(); 1914 } 1915 1916 while (!isDelimiter((c = buf.nextChar()))) { 1917 if (c == 0) { 1918 break; 1919 } 1920 killed.append(c); 1921 delete(); 1922 } 1923 1924 String copy = killed.toString(); 1925 killRing.add(copy); 1926 1927 return true; 1928 } 1929 1930 private boolean capitalizeWord() throws IOException { 1931 boolean first = true; 1932 int i = 1; 1933 char c; 1934 while (buf.cursor + i - 1< buf.length() && !isDelimiter((c = buf.buffer.charAt(buf.cursor + i - 1)))) { 1935 buf.buffer.setCharAt(buf.cursor + i - 1, first ? Character.toUpperCase(c) : Character.toLowerCase(c)); 1936 first = false; 1937 i++; 1938 } 1939 drawBuffer(); 1940 moveCursor(i - 1); 1941 return true; 1942 } 1943 1944 private boolean upCaseWord() throws IOException { 1945 int i = 1; 1946 char c; 1947 while (buf.cursor + i - 1 < buf.length() && !isDelimiter((c = buf.buffer.charAt(buf.cursor + i - 1)))) { 1948 buf.buffer.setCharAt(buf.cursor + i - 1, Character.toUpperCase(c)); 1949 i++; 1950 } 1951 drawBuffer(); 1952 moveCursor(i - 1); 1953 return true; 1954 } 1955 1956 private boolean downCaseWord() throws IOException { 1957 int i = 1; 1958 char c; 1959 while (buf.cursor + i - 1 < buf.length() && !isDelimiter((c = buf.buffer.charAt(buf.cursor + i - 1)))) { 1960 buf.buffer.setCharAt(buf.cursor + i - 1, Character.toLowerCase(c)); 1961 i++; 1962 } 1963 drawBuffer(); 1964 moveCursor(i - 1); 1965 return true; 1966 } 1967 1968 /** 1969 * Performs character transpose. The character prior to the cursor and the 1970 * character under the cursor are swapped and the cursor is advanced one 1971 * character unless you are already at the end of the line. 1972 * 1973 * @param count The number of times to perform the transpose 1974 * @return true if the operation succeeded, false otherwise (e.g. transpose 1975 * cannot happen at the beginning of the line). 1976 * @throws IOException 1977 */ 1978 private boolean transposeChars(int count) throws IOException { 1979 for (; count > 0; --count) { 1980 if (buf.cursor == 0 || buf.cursor == buf.buffer.length()) { 1981 return false; 1982 } 1983 1984 int first = buf.cursor-1; 1985 int second = buf.cursor; 1986 1987 char tmp = buf.buffer.charAt (first); 1988 buf.buffer.setCharAt(first, buf.buffer.charAt(second)); 1989 buf.buffer.setCharAt(second, tmp); 1990 1991 // This could be done more efficiently by only re-drawing at the end. 1992 moveInternal(-1); 1993 drawBuffer(); 1994 moveInternal(2); 1995 } 1996 1997 return true; 1998 } 1999 2000 public boolean isKeyMap(String name) { 2001 // Current keymap. 2002 KeyMap map = consoleKeys.getKeys(); 2003 KeyMap mapByName = consoleKeys.getKeyMaps().get(name); 2004 2005 if (mapByName == null) 2006 return false; 2007 2008 /* 2009 * This may not be safe to do, but there doesn't appear to be a 2010 * clean way to find this information out. 2011 */ 2012 return map == mapByName; 2013 } 2014 2015 2016 /** 2017 * The equivalent of hitting <RET>. The line is considered 2018 * complete and is returned. 2019 * 2020 * @return The completed line of text. 2021 * @throws IOException 2022 */ 2023 public String accept() throws IOException { 2024 moveToEnd(); 2025 println(); // output newline 2026 flush(); 2027 return finishBuffer(); 2028 } 2029 2030 private void abort() throws IOException { 2031 beep(); 2032 buf.clear(); 2033 println(); 2034 redrawLine(); 2035 } 2036 2037 /** 2038 * Move the cursor <i>where</i> characters. 2039 * 2040 * @param num If less than 0, move abs(<i>where</i>) to the left, otherwise move <i>where</i> to the right. 2041 * @return The number of spaces we moved 2042 */ 2043 public int moveCursor(final int num) throws IOException { 2044 int where = num; 2045 2046 if ((buf.cursor == 0) && (where <= 0)) { 2047 return 0; 2048 } 2049 2050 if ((buf.cursor == buf.buffer.length()) && (where >= 0)) { 2051 return 0; 2052 } 2053 2054 if ((buf.cursor + where) < 0) { 2055 where = -buf.cursor; 2056 } 2057 else if ((buf.cursor + where) > buf.buffer.length()) { 2058 where = buf.buffer.length() - buf.cursor; 2059 } 2060 2061 moveInternal(where); 2062 2063 return where; 2064 } 2065 2066 /** 2067 * Move the cursor <i>where</i> characters, without checking the current buffer. 2068 * 2069 * @param where the number of characters to move to the right or left. 2070 */ 2071 private void moveInternal(final int where) throws IOException { 2072 // debug ("move cursor " + where + " (" 2073 // + buf.cursor + " => " + (buf.cursor + where) + ")"); 2074 buf.cursor += where; 2075 2076 if (terminal.isAnsiSupported()) { 2077 if (where < 0) { 2078 back(Math.abs(where)); 2079 } else { 2080 int width = getTerminal().getWidth(); 2081 int cursor = getCursorPosition(); 2082 int oldLine = (cursor - where) / width; 2083 int newLine = cursor / width; 2084 if (newLine > oldLine) { 2085 printAnsiSequence((newLine - oldLine) + "B"); 2086 } 2087 printAnsiSequence(1 +(cursor % width) + "G"); 2088 } 2089 // flush(); 2090 return; 2091 } 2092 2093 char c; 2094 2095 if (where < 0) { 2096 int len = 0; 2097 for (int i = buf.cursor; i < buf.cursor - where; i++) { 2098 if (buf.buffer.charAt(i) == '\t') { 2099 len += TAB_WIDTH; 2100 } 2101 else { 2102 len++; 2103 } 2104 } 2105 2106 char chars[] = new char[len]; 2107 Arrays.fill(chars, BACKSPACE); 2108 out.write(chars); 2109 2110 return; 2111 } 2112 else if (buf.cursor == 0) { 2113 return; 2114 } 2115 else if (mask != null) { 2116 c = mask; 2117 } 2118 else { 2119 print(buf.buffer.substring(buf.cursor - where, buf.cursor).toCharArray()); 2120 return; 2121 } 2122 2123 // null character mask: don't output anything 2124 if (mask == NULL_MASK) { 2125 return; 2126 } 2127 2128 print(c, Math.abs(where)); 2129 } 2130 2131 // FIXME: replace() is not used 2132 2133 public final boolean replace(final int num, final String replacement) { 2134 buf.buffer.replace(buf.cursor - num, buf.cursor, replacement); 2135 try { 2136 moveCursor(-num); 2137 drawBuffer(Math.max(0, num - replacement.length())); 2138 moveCursor(replacement.length()); 2139 } 2140 catch (IOException e) { 2141 e.printStackTrace(); 2142 return false; 2143 } 2144 return true; 2145 } 2146 2147 /** 2148 * Read a character from the console. 2149 * 2150 * @return the character, or -1 if an EOF is received. 2151 */ 2152 public final int readCharacter() throws IOException { 2153 int c = reader.read(); 2154 if (c >= 0) { 2155 Log.trace("Keystroke: ", c); 2156 // clear any echo characters 2157 if (terminal.isSupported()) { 2158 clearEcho(c); 2159 } 2160 } 2161 return c; 2162 } 2163 2164 /** 2165 * Clear the echoed characters for the specified character code. 2166 */ 2167 private int clearEcho(final int c) throws IOException { 2168 // if the terminal is not echoing, then ignore 2169 if (!terminal.isEchoEnabled()) { 2170 return 0; 2171 } 2172 2173 // otherwise, clear 2174 int num = countEchoCharacters(c); 2175 back(num); 2176 drawBuffer(num); 2177 2178 return num; 2179 } 2180 2181 private int countEchoCharacters(final int c) { 2182 // tabs as special: we need to determine the number of spaces 2183 // to cancel based on what out current cursor position is 2184 if (c == 9) { 2185 int tabStop = 8; // will this ever be different? 2186 int position = getCursorPosition(); 2187 2188 return tabStop - (position % tabStop); 2189 } 2190 2191 return getPrintableCharacters(c).length(); 2192 } 2193 2194 /** 2195 * Return the number of characters that will be printed when the specified 2196 * character is echoed to the screen 2197 * 2198 * Adapted from cat by Torbjorn Granlund, as repeated in stty by David MacKenzie. 2199 */ 2200 private StringBuilder getPrintableCharacters(final int ch) { 2201 StringBuilder sbuff = new StringBuilder(); 2202 2203 if (ch >= 32) { 2204 if (ch < 127) { 2205 sbuff.append(ch); 2206 } 2207 else if (ch == 127) { 2208 sbuff.append('^'); 2209 sbuff.append('?'); 2210 } 2211 else { 2212 sbuff.append('M'); 2213 sbuff.append('-'); 2214 2215 if (ch >= (128 + 32)) { 2216 if (ch < (128 + 127)) { 2217 sbuff.append((char) (ch - 128)); 2218 } 2219 else { 2220 sbuff.append('^'); 2221 sbuff.append('?'); 2222 } 2223 } 2224 else { 2225 sbuff.append('^'); 2226 sbuff.append((char) (ch - 128 + 64)); 2227 } 2228 } 2229 } 2230 else { 2231 sbuff.append('^'); 2232 sbuff.append((char) (ch + 64)); 2233 } 2234 2235 return sbuff; 2236 } 2237 2238 public final int readCharacter(final char... allowed) throws IOException { 2239 // if we restrict to a limited set and the current character is not in the set, then try again. 2240 char c; 2241 2242 Arrays.sort(allowed); // always need to sort before binarySearch 2243 2244 while (Arrays.binarySearch(allowed, c = (char) readCharacter()) < 0) { 2245 // nothing 2246 } 2247 2248 return c; 2249 } 2250 2251 // 2252 // Key Bindings 2253 // 2254 2255 public static final String JLINE_COMPLETION_THRESHOLD = "jline.completion.threshold"; 2256 2257 // 2258 // Line Reading 2259 // 2260 2261 /** 2262 * Read the next line and return the contents of the buffer. 2263 */ 2264 public String readLine() throws IOException { 2265 return readLine((String) null); 2266 } 2267 2268 /** 2269 * Read the next line with the specified character mask. If null, then 2270 * characters will be echoed. If 0, then no characters will be echoed. 2271 */ 2272 public String readLine(final Character mask) throws IOException { 2273 return readLine(null, mask); 2274 } 2275 2276 public String readLine(final String prompt) throws IOException { 2277 return readLine(prompt, null); 2278 } 2279 2280 /** 2281 * Sets the current keymap by name. Supported keymaps are "emacs", 2282 * "vi-insert", "vi-move". 2283 * @param name The name of the keymap to switch to 2284 * @return true if the keymap was set, or false if the keymap is 2285 * not recognized. 2286 */ 2287 public boolean setKeyMap(String name) { 2288 return consoleKeys.setKeyMap(name); 2289 } 2290 2291 /** 2292 * Returns the name of the current key mapping. 2293 * @return the name of the key mapping. This will be the canonical name 2294 * of the current mode of the key map and may not reflect the name that 2295 * was used with {@link #setKeyMap(String)}. 2296 */ 2297 public String getKeyMap() { 2298 return consoleKeys.getKeys().getName(); 2299 } 2300 2301 /** 2302 * Read a line from the <i>in</i> {@link InputStream}, and return the line 2303 * (without any trailing newlines). 2304 * 2305 * @param prompt The prompt to issue to the console, may be null. 2306 * @return A line that is read from the terminal, or null if there was null input (e.g., <i>CTRL-D</i> 2307 * was pressed). 2308 */ 2309 public String readLine(String prompt, final Character mask) throws IOException { 2310 // prompt may be null 2311 // mask may be null 2312 2313 /* 2314 * This is the accumulator for VI-mode repeat count. That is, while in 2315 * move mode, if you type 30x it will delete 30 characters. This is 2316 * where the "30" is accumulated until the command is struck. 2317 */ 2318 int repeatCount = 0; 2319 2320 // FIXME: This blows, each call to readLine will reset the console's state which doesn't seem very nice. 2321 this.mask = mask; 2322 if (prompt != null) { 2323 setPrompt(prompt); 2324 } 2325 else { 2326 prompt = getPrompt(); 2327 } 2328 2329 try { 2330 if (!terminal.isSupported()) { 2331 beforeReadLine(prompt, mask); 2332 } 2333 2334 if (prompt != null && prompt.length() > 0) { 2335 out.write(prompt); 2336 out.flush(); 2337 } 2338 2339 // if the terminal is unsupported, just use plain-java reading 2340 if (!terminal.isSupported()) { 2341 return readLineSimple(); 2342 } 2343 2344 if (handleUserInterrupt && (terminal instanceof UnixTerminal)) { 2345 ((UnixTerminal) terminal).disableInterruptCharacter(); 2346 } 2347 2348 String originalPrompt = this.prompt; 2349 2350 state = State.NORMAL; 2351 2352 boolean success = true; 2353 2354 StringBuilder sb = new StringBuilder(); 2355 Stack<Character> pushBackChar = new Stack<Character>(); 2356 while (true) { 2357 int c = pushBackChar.isEmpty() ? readCharacter() : pushBackChar.pop (); 2358 if (c == -1) { 2359 return null; 2360 } 2361 sb.appendCodePoint(c); 2362 2363 if (recording) { 2364 macro += new String(new int[]{c}, 0, 1); 2365 } 2366 2367 Object o = getKeys().getBound( sb ); 2368 /* 2369 * The kill ring keeps record of whether or not the 2370 * previous command was a yank or a kill. We reset 2371 * that state here if needed. 2372 */ 2373 if (!recording && !(o instanceof KeyMap)) { 2374 if (o != Operation.YANK_POP && o != Operation.YANK) { 2375 killRing.resetLastYank(); 2376 } 2377 if (o != Operation.KILL_LINE && o != Operation.KILL_WHOLE_LINE 2378 && o != Operation.BACKWARD_KILL_WORD && o != Operation.KILL_WORD 2379 && o != Operation.UNIX_LINE_DISCARD && o != Operation.UNIX_WORD_RUBOUT) { 2380 killRing.resetLastKill(); 2381 } 2382 } 2383 2384 if (o == Operation.DO_LOWERCASE_VERSION) { 2385 sb.setLength( sb.length() - 1); 2386 sb.append( Character.toLowerCase( (char) c )); 2387 o = getKeys().getBound( sb ); 2388 } 2389 2390 /* 2391 * A KeyMap indicates that the key that was struck has a 2392 * number of keys that can follow it as indicated in the 2393 * map. This is used primarily for Emacs style ESC-META-x 2394 * lookups. Since more keys must follow, go back to waiting 2395 * for the next key. 2396 */ 2397 if ( o instanceof KeyMap ) { 2398 /* 2399 * The ESC key (#27) is special in that it is ambiguous until 2400 * you know what is coming next. The ESC could be a literal 2401 * escape, like the user entering vi-move mode, or it could 2402 * be part of a terminal control sequence. The following 2403 * logic attempts to disambiguate things in the same 2404 * fashion as regular vi or readline. 2405 * 2406 * When ESC is encountered and there is no other pending 2407 * character in the pushback queue, then attempt to peek 2408 * into the input stream (if the feature is enabled) for 2409 * 150ms. If nothing else is coming, then assume it is 2410 * not a terminal control sequence, but a raw escape. 2411 */ 2412 if (c == 27 2413 && pushBackChar.isEmpty() 2414 && in.isNonBlockingEnabled() 2415 && in.peek(escapeTimeout) == -2) { 2416 o = ((KeyMap) o).getAnotherKey(); 2417 if (o == null || o instanceof KeyMap) { 2418 continue; 2419 } 2420 sb.setLength(0); 2421 } 2422 else { 2423 continue; 2424 } 2425 } 2426 2427 /* 2428 * If we didn't find a binding for the key and there is 2429 * more than one character accumulated then start checking 2430 * the largest span of characters from the beginning to 2431 * see if there is a binding for them. 2432 * 2433 * For example if our buffer has ESC,CTRL-M,C the getBound() 2434 * called previously indicated that there is no binding for 2435 * this sequence, so this then checks ESC,CTRL-M, and failing 2436 * that, just ESC. Each keystroke that is pealed off the end 2437 * during these tests is stuffed onto the pushback buffer so 2438 * they won't be lost. 2439 * 2440 * If there is no binding found, then we go back to waiting for 2441 * input. 2442 */ 2443 while ( o == null && sb.length() > 0 ) { 2444 c = sb.charAt( sb.length() - 1 ); 2445 sb.setLength( sb.length() - 1 ); 2446 Object o2 = getKeys().getBound( sb ); 2447 if ( o2 instanceof KeyMap ) { 2448 o = ((KeyMap) o2).getAnotherKey(); 2449 if ( o == null ) { 2450 continue; 2451 } else { 2452 pushBackChar.push( (char) c ); 2453 } 2454 } 2455 } 2456 2457 if ( o == null ) { 2458 continue; 2459 } 2460 Log.trace("Binding: ", o); 2461 2462 2463 // Handle macros 2464 if (o instanceof String) { 2465 String macro = (String) o; 2466 for (int i = 0; i < macro.length(); i++) { 2467 pushBackChar.push(macro.charAt(macro.length() - 1 - i)); 2468 } 2469 sb.setLength( 0 ); 2470 continue; 2471 } 2472 2473 // Handle custom callbacks 2474 //original code: 2475 // if (o instanceof ActionListener) { 2476 // ((ActionListener) o).actionPerformed(null); 2477 // sb.setLength( 0 ); 2478 // continue; 2479 // } 2480 //using reflection to avoid dependency on java.desktop: 2481 try { 2482 Class<?> actionListener = 2483 Class.forName("java.awt.event.ActionListener", false, ClassLoader.getSystemClassLoader()); 2484 Class<?> actionEvent = 2485 Class.forName("java.awt.event.ActionEvent", false, ClassLoader.getSystemClassLoader()); 2486 if (actionListener.isAssignableFrom(o.getClass())) { 2487 Method actionPerformed = 2488 actionListener.getMethod("actionPerformed", actionEvent); 2489 try { 2490 actionPerformed.invoke(o, (Object) null); 2491 } catch (InvocationTargetException ex ) { 2492 Log.error("Exception while running registered action", ex); 2493 } 2494 sb.setLength( 0 ); 2495 continue; 2496 } 2497 } catch (ReflectiveOperationException ex) { 2498 //ignore 2499 } 2500 2501 // Search mode. 2502 // 2503 // Note that we have to do this first, because if there is a command 2504 // not linked to a search command, we leave the search mode and fall 2505 // through to the normal state. 2506 if (state == State.SEARCH || state == State.FORWARD_SEARCH) { 2507 int cursorDest = -1; 2508 switch ( ((Operation) o )) { 2509 case ABORT: 2510 state = State.NORMAL; 2511 buf.clear(); 2512 buf.buffer.append(searchTerm); 2513 break; 2514 2515 case REVERSE_SEARCH_HISTORY: 2516 state = State.SEARCH; 2517 if (searchTerm.length() == 0) { 2518 searchTerm.append(previousSearchTerm); 2519 } 2520 2521 if (searchIndex > 0) { 2522 searchIndex = searchBackwards(searchTerm.toString(), searchIndex); 2523 } 2524 break; 2525 2526 case FORWARD_SEARCH_HISTORY: 2527 state = State.FORWARD_SEARCH; 2528 if (searchTerm.length() == 0) { 2529 searchTerm.append(previousSearchTerm); 2530 } 2531 2532 if (searchIndex > -1 && searchIndex < history.size() - 1) { 2533 searchIndex = searchForwards(searchTerm.toString(), searchIndex); 2534 } 2535 break; 2536 2537 case BACKWARD_DELETE_CHAR: 2538 if (searchTerm.length() > 0) { 2539 searchTerm.deleteCharAt(searchTerm.length() - 1); 2540 if (state == State.SEARCH) { 2541 searchIndex = searchBackwards(searchTerm.toString()); 2542 } else { 2543 searchIndex = searchForwards(searchTerm.toString()); 2544 } 2545 } 2546 break; 2547 2548 case SELF_INSERT: 2549 searchTerm.appendCodePoint(c); 2550 if (state == State.SEARCH) { 2551 searchIndex = searchBackwards(searchTerm.toString()); 2552 } else { 2553 searchIndex = searchForwards(searchTerm.toString()); 2554 } 2555 break; 2556 2557 default: 2558 // Set buffer and cursor position to the found string. 2559 if (searchIndex != -1) { 2560 history.moveTo(searchIndex); 2561 // set cursor position to the found string 2562 cursorDest = history.current().toString().indexOf(searchTerm.toString()); 2563 } 2564 state = State.NORMAL; 2565 break; 2566 } 2567 2568 // if we're still in search mode, print the search status 2569 if (state == State.SEARCH || state == State.FORWARD_SEARCH) { 2570 if (searchTerm.length() == 0) { 2571 if (state == State.SEARCH) { 2572 printSearchStatus("", ""); 2573 } else { 2574 printForwardSearchStatus("", ""); 2575 } 2576 searchIndex = -1; 2577 } else { 2578 if (searchIndex == -1) { 2579 beep(); 2580 printSearchStatus(searchTerm.toString(), ""); 2581 } else if (state == State.SEARCH) { 2582 printSearchStatus(searchTerm.toString(), history.get(searchIndex).toString()); 2583 } else { 2584 printForwardSearchStatus(searchTerm.toString(), history.get(searchIndex).toString()); 2585 } 2586 } 2587 } 2588 // otherwise, restore the line 2589 else { 2590 restoreLine(originalPrompt, cursorDest); 2591 } 2592 } 2593 if (state != State.SEARCH && state != State.FORWARD_SEARCH) { 2594 /* 2595 * If this is still false at the end of the switch, then 2596 * we reset our repeatCount to 0. 2597 */ 2598 boolean isArgDigit = false; 2599 2600 /* 2601 * Every command that can be repeated a specified number 2602 * of times, needs to know how many times to repeat, so 2603 * we figure that out here. 2604 */ 2605 int count = (repeatCount == 0) ? 1 : repeatCount; 2606 2607 /* 2608 * Default success to true. You only need to explicitly 2609 * set it if something goes wrong. 2610 */ 2611 success = true; 2612 2613 if (o instanceof Operation) { 2614 Operation op = (Operation)o; 2615 /* 2616 * Current location of the cursor (prior to the operation). 2617 * These are used by vi *-to operation (e.g. delete-to) 2618 * so we know where we came from. 2619 */ 2620 int cursorStart = buf.cursor; 2621 State origState = state; 2622 2623 /* 2624 * If we are on a "vi" movement based operation, then we 2625 * need to restrict the sets of inputs pretty heavily. 2626 */ 2627 if (state == State.VI_CHANGE_TO 2628 || state == State.VI_YANK_TO 2629 || state == State.VI_DELETE_TO) { 2630 2631 op = viDeleteChangeYankToRemap(op); 2632 } 2633 2634 switch ( op ) { 2635 case COMPLETE: // tab 2636 // There is an annoyance with tab completion in that 2637 // sometimes the user is actually pasting input in that 2638 // has physical tabs in it. This attempts to look at how 2639 // quickly a character follows the tab, if the character 2640 // follows *immediately*, we assume it is a tab literal. 2641 boolean isTabLiteral = false; 2642 if (copyPasteDetection 2643 && c == 9 2644 && (!pushBackChar.isEmpty() 2645 || (in.isNonBlockingEnabled() && in.peek(escapeTimeout) != -2))) { 2646 isTabLiteral = true; 2647 } 2648 2649 if (! isTabLiteral) { 2650 success = complete(); 2651 } 2652 else { 2653 putString(sb); 2654 } 2655 break; 2656 2657 case POSSIBLE_COMPLETIONS: 2658 printCompletionCandidates(); 2659 break; 2660 2661 case BEGINNING_OF_LINE: 2662 success = setCursorPosition(0); 2663 break; 2664 2665 case YANK: 2666 success = yank(); 2667 break; 2668 2669 case YANK_POP: 2670 success = yankPop(); 2671 break; 2672 2673 case KILL_LINE: // CTRL-K 2674 success = killLine(); 2675 break; 2676 2677 case KILL_WHOLE_LINE: 2678 success = setCursorPosition(0) && killLine(); 2679 break; 2680 2681 case CLEAR_SCREEN: // CTRL-L 2682 success = clearScreen(); 2683 redrawLine(); 2684 break; 2685 2686 case OVERWRITE_MODE: 2687 buf.setOverTyping(!buf.isOverTyping()); 2688 break; 2689 2690 case SELF_INSERT: 2691 putString(sb); 2692 break; 2693 2694 case ACCEPT_LINE: 2695 return accept(); 2696 2697 case ABORT: 2698 if (searchTerm == null) { 2699 abort(); 2700 } 2701 break; 2702 2703 case INTERRUPT: 2704 if (handleUserInterrupt) { 2705 println(); 2706 flush(); 2707 String partialLine = buf.buffer.toString(); 2708 buf.clear(); 2709 history.moveToEnd(); 2710 throw new UserInterruptException(partialLine); 2711 } 2712 break; 2713 2714 /* 2715 * VI_MOVE_ACCEPT_LINE is the result of an ENTER 2716 * while in move mode. This is the same as a normal 2717 * ACCEPT_LINE, except that we need to enter 2718 * insert mode as well. 2719 */ 2720 case VI_MOVE_ACCEPT_LINE: 2721 consoleKeys.setKeyMap(KeyMap.VI_INSERT); 2722 return accept(); 2723 2724 case BACKWARD_WORD: 2725 success = previousWord(); 2726 break; 2727 2728 case FORWARD_WORD: 2729 success = nextWord(); 2730 break; 2731 2732 case PREVIOUS_HISTORY: 2733 success = moveHistory(false); 2734 break; 2735 2736 /* 2737 * According to bash/readline move through history 2738 * in "vi" mode will move the cursor to the 2739 * start of the line. If there is no previous 2740 * history, then the cursor doesn't move. 2741 */ 2742 case VI_PREVIOUS_HISTORY: 2743 success = moveHistory(false, count) 2744 && setCursorPosition(0); 2745 break; 2746 2747 case NEXT_HISTORY: 2748 success = moveHistory(true); 2749 break; 2750 2751 /* 2752 * According to bash/readline move through history 2753 * in "vi" mode will move the cursor to the 2754 * start of the line. If there is no next history, 2755 * then the cursor doesn't move. 2756 */ 2757 case VI_NEXT_HISTORY: 2758 success = moveHistory(true, count) 2759 && setCursorPosition(0); 2760 break; 2761 2762 case BACKWARD_DELETE_CHAR: // backspace 2763 success = backspace(); 2764 break; 2765 2766 case EXIT_OR_DELETE_CHAR: 2767 if (buf.buffer.length() == 0) { 2768 return null; 2769 } 2770 success = deleteCurrentCharacter(); 2771 break; 2772 2773 case DELETE_CHAR: // delete 2774 success = deleteCurrentCharacter(); 2775 break; 2776 2777 case BACKWARD_CHAR: 2778 success = moveCursor(-(count)) != 0; 2779 break; 2780 2781 case FORWARD_CHAR: 2782 success = moveCursor(count) != 0; 2783 break; 2784 2785 case UNIX_LINE_DISCARD: 2786 success = resetLine(); 2787 break; 2788 2789 case UNIX_WORD_RUBOUT: 2790 success = unixWordRubout(count); 2791 break; 2792 2793 case BACKWARD_KILL_WORD: 2794 success = deletePreviousWord(); 2795 break; 2796 2797 case KILL_WORD: 2798 success = deleteNextWord(); 2799 break; 2800 2801 case BEGINNING_OF_HISTORY: 2802 success = history.moveToFirst(); 2803 if (success) { 2804 setBuffer(history.current()); 2805 } 2806 break; 2807 2808 case END_OF_HISTORY: 2809 success = history.moveToLast(); 2810 if (success) { 2811 setBuffer(history.current()); 2812 } 2813 break; 2814 2815 case HISTORY_SEARCH_BACKWARD: 2816 searchTerm = new StringBuffer(buf.upToCursor()); 2817 searchIndex = searchBackwards(searchTerm.toString(), history.index(), true); 2818 2819 if (searchIndex == -1) { 2820 beep(); 2821 } else { 2822 // Maintain cursor position while searching. 2823 success = history.moveTo(searchIndex); 2824 if (success) { 2825 setBufferKeepPos(history.current()); 2826 } 2827 } 2828 break; 2829 2830 case HISTORY_SEARCH_FORWARD: 2831 searchTerm = new StringBuffer(buf.upToCursor()); 2832 int index = history.index() + 1; 2833 2834 if (index == history.size()) { 2835 history.moveToEnd(); 2836 setBufferKeepPos(searchTerm.toString()); 2837 } else if (index < history.size()) { 2838 searchIndex = searchForwards(searchTerm.toString(), index, true); 2839 if (searchIndex == -1) { 2840 beep(); 2841 } else { 2842 // Maintain cursor position while searching. 2843 success = history.moveTo(searchIndex); 2844 if (success) { 2845 setBufferKeepPos(history.current()); 2846 } 2847 } 2848 } 2849 break; 2850 2851 case REVERSE_SEARCH_HISTORY: 2852 if (searchTerm != null) { 2853 previousSearchTerm = searchTerm.toString(); 2854 } 2855 searchTerm = new StringBuffer(buf.buffer); 2856 state = State.SEARCH; 2857 if (searchTerm.length() > 0) { 2858 searchIndex = searchBackwards(searchTerm.toString()); 2859 if (searchIndex == -1) { 2860 beep(); 2861 } 2862 printSearchStatus(searchTerm.toString(), 2863 searchIndex > -1 ? history.get(searchIndex).toString() : ""); 2864 } else { 2865 searchIndex = -1; 2866 printSearchStatus("", ""); 2867 } 2868 break; 2869 2870 case FORWARD_SEARCH_HISTORY: 2871 if (searchTerm != null) { 2872 previousSearchTerm = searchTerm.toString(); 2873 } 2874 searchTerm = new StringBuffer(buf.buffer); 2875 state = State.FORWARD_SEARCH; 2876 if (searchTerm.length() > 0) { 2877 searchIndex = searchForwards(searchTerm.toString()); 2878 if (searchIndex == -1) { 2879 beep(); 2880 } 2881 printForwardSearchStatus(searchTerm.toString(), 2882 searchIndex > -1 ? history.get(searchIndex).toString() : ""); 2883 } else { 2884 searchIndex = -1; 2885 printForwardSearchStatus("", ""); 2886 } 2887 break; 2888 2889 case CAPITALIZE_WORD: 2890 success = capitalizeWord(); 2891 break; 2892 2893 case UPCASE_WORD: 2894 success = upCaseWord(); 2895 break; 2896 2897 case DOWNCASE_WORD: 2898 success = downCaseWord(); 2899 break; 2900 2901 case END_OF_LINE: 2902 success = moveToEnd(); 2903 break; 2904 2905 case TAB_INSERT: 2906 putString( "\t" ); 2907 break; 2908 2909 case RE_READ_INIT_FILE: 2910 consoleKeys.loadKeys(appName, inputrcUrl); 2911 break; 2912 2913 case START_KBD_MACRO: 2914 recording = true; 2915 break; 2916 2917 case END_KBD_MACRO: 2918 recording = false; 2919 macro = macro.substring(0, macro.length() - sb.length()); 2920 break; 2921 2922 case CALL_LAST_KBD_MACRO: 2923 for (int i = 0; i < macro.length(); i++) { 2924 pushBackChar.push(macro.charAt(macro.length() - 1 - i)); 2925 } 2926 sb.setLength( 0 ); 2927 break; 2928 2929 case VI_EDITING_MODE: 2930 consoleKeys.setKeyMap(KeyMap.VI_INSERT); 2931 break; 2932 2933 case VI_MOVEMENT_MODE: 2934 /* 2935 * If we are re-entering move mode from an 2936 * aborted yank-to, delete-to, change-to then 2937 * don't move the cursor back. The cursor is 2938 * only move on an expclit entry to movement 2939 * mode. 2940 */ 2941 if (state == State.NORMAL) { 2942 moveCursor(-1); 2943 } 2944 consoleKeys.setKeyMap(KeyMap.VI_MOVE); 2945 break; 2946 2947 case VI_INSERTION_MODE: 2948 consoleKeys.setKeyMap(KeyMap.VI_INSERT); 2949 break; 2950 2951 case VI_APPEND_MODE: 2952 moveCursor(1); 2953 consoleKeys.setKeyMap(KeyMap.VI_INSERT); 2954 break; 2955 2956 case VI_APPEND_EOL: 2957 success = moveToEnd(); 2958 consoleKeys.setKeyMap(KeyMap.VI_INSERT); 2959 break; 2960 2961 /* 2962 * Handler for CTRL-D. Attempts to follow readline 2963 * behavior. If the line is empty, then it is an EOF 2964 * otherwise it is as if the user hit enter. 2965 */ 2966 case VI_EOF_MAYBE: 2967 if (buf.buffer.length() == 0) { 2968 return null; 2969 } 2970 return accept(); 2971 2972 case TRANSPOSE_CHARS: 2973 success = transposeChars(count); 2974 break; 2975 2976 case INSERT_COMMENT: 2977 return insertComment (false); 2978 2979 case INSERT_CLOSE_CURLY: 2980 insertClose("}"); 2981 break; 2982 2983 case INSERT_CLOSE_PAREN: 2984 insertClose(")"); 2985 break; 2986 2987 case INSERT_CLOSE_SQUARE: 2988 insertClose("]"); 2989 break; 2990 2991 case VI_INSERT_COMMENT: 2992 return insertComment (true); 2993 2994 case VI_MATCH: 2995 success = viMatch (); 2996 break; 2997 2998 case VI_SEARCH: 2999 int lastChar = viSearch(sb.charAt (0)); 3000 if (lastChar != -1) { 3001 pushBackChar.push((char)lastChar); 3002 } 3003 break; 3004 3005 case VI_ARG_DIGIT: 3006 repeatCount = (repeatCount * 10) + sb.charAt(0) - '0'; 3007 isArgDigit = true; 3008 break; 3009 3010 case VI_BEGNNING_OF_LINE_OR_ARG_DIGIT: 3011 if (repeatCount > 0) { 3012 repeatCount = (repeatCount * 10) + sb.charAt(0) - '0'; 3013 isArgDigit = true; 3014 } 3015 else { 3016 success = setCursorPosition(0); 3017 } 3018 break; 3019 3020 case VI_FIRST_PRINT: 3021 success = setCursorPosition(0) && viNextWord(1); 3022 break; 3023 3024 case VI_PREV_WORD: 3025 success = viPreviousWord(count); 3026 break; 3027 3028 case VI_NEXT_WORD: 3029 success = viNextWord(count); 3030 break; 3031 3032 case VI_END_WORD: 3033 success = viEndWord(count); 3034 break; 3035 3036 case VI_INSERT_BEG: 3037 success = setCursorPosition(0); 3038 consoleKeys.setKeyMap(KeyMap.VI_INSERT); 3039 break; 3040 3041 case VI_RUBOUT: 3042 success = viRubout(count); 3043 break; 3044 3045 case VI_DELETE: 3046 success = viDelete(count); 3047 break; 3048 3049 case VI_DELETE_TO: 3050 /* 3051 * This is a weird special case. In vi 3052 * "dd" deletes the current line. So if we 3053 * get a delete-to, followed by a delete-to, 3054 * we delete the line. 3055 */ 3056 if (state == State.VI_DELETE_TO) { 3057 success = setCursorPosition(0) && killLine(); 3058 state = origState = State.NORMAL; 3059 } 3060 else { 3061 state = State.VI_DELETE_TO; 3062 } 3063 break; 3064 3065 case VI_YANK_TO: 3066 // Similar to delete-to, a "yy" yanks the whole line. 3067 if (state == State.VI_YANK_TO) { 3068 yankBuffer = buf.buffer.toString(); 3069 state = origState = State.NORMAL; 3070 } 3071 else { 3072 state = State.VI_YANK_TO; 3073 } 3074 break; 3075 3076 case VI_CHANGE_TO: 3077 if (state == State.VI_CHANGE_TO) { 3078 success = setCursorPosition(0) && killLine(); 3079 state = origState = State.NORMAL; 3080 consoleKeys.setKeyMap(KeyMap.VI_INSERT); 3081 } 3082 else { 3083 state = State.VI_CHANGE_TO; 3084 } 3085 break; 3086 3087 case VI_KILL_WHOLE_LINE: 3088 success = setCursorPosition(0) && killLine(); 3089 consoleKeys.setKeyMap(KeyMap.VI_INSERT); 3090 break; 3091 3092 case VI_PUT: 3093 success = viPut(count); 3094 break; 3095 3096 case VI_CHAR_SEARCH: { 3097 // ';' and ',' don't need another character. They indicate repeat next or repeat prev. 3098 int searchChar = (c != ';' && c != ',') 3099 ? (pushBackChar.isEmpty() 3100 ? readCharacter() 3101 : pushBackChar.pop ()) 3102 : 0; 3103 3104 success = viCharSearch(count, c, searchChar); 3105 } 3106 break; 3107 3108 case VI_CHANGE_CASE: 3109 success = viChangeCase(count); 3110 break; 3111 3112 case VI_CHANGE_CHAR: 3113 success = viChangeChar(count, 3114 pushBackChar.isEmpty() 3115 ? readCharacter() 3116 : pushBackChar.pop()); 3117 break; 3118 3119 case VI_DELETE_TO_EOL: 3120 success = viDeleteTo(buf.cursor, buf.buffer.length(), false); 3121 break; 3122 3123 case VI_CHANGE_TO_EOL: 3124 success = viDeleteTo(buf.cursor, buf.buffer.length(), true); 3125 consoleKeys.setKeyMap(KeyMap.VI_INSERT); 3126 break; 3127 3128 case EMACS_EDITING_MODE: 3129 consoleKeys.setKeyMap(KeyMap.EMACS); 3130 break; 3131 3132 default: 3133 break; 3134 } 3135 3136 /* 3137 * If we were in a yank-to, delete-to, move-to 3138 * when this operation started, then fall back to 3139 */ 3140 if (origState != State.NORMAL) { 3141 if (origState == State.VI_DELETE_TO) { 3142 success = viDeleteTo(cursorStart, buf.cursor, false); 3143 } 3144 else if (origState == State.VI_CHANGE_TO) { 3145 success = viDeleteTo(cursorStart, buf.cursor, true); 3146 consoleKeys.setKeyMap(KeyMap.VI_INSERT); 3147 } 3148 else if (origState == State.VI_YANK_TO) { 3149 success = viYankTo(cursorStart, buf.cursor); 3150 } 3151 state = State.NORMAL; 3152 } 3153 3154 /* 3155 * Another subtly. The check for the NORMAL state is 3156 * to ensure that we do not clear out the repeat 3157 * count when in delete-to, yank-to, or move-to modes. 3158 */ 3159 if (state == State.NORMAL && !isArgDigit) { 3160 /* 3161 * If the operation performed wasn't a vi argument 3162 * digit, then clear out the current repeatCount; 3163 */ 3164 repeatCount = 0; 3165 } 3166 3167 if (state != State.SEARCH && state != State.FORWARD_SEARCH) { 3168 previousSearchTerm = ""; 3169 searchTerm = null; 3170 searchIndex = -1; 3171 } 3172 } 3173 } 3174 if (!success) { 3175 beep(); 3176 } 3177 sb.setLength( 0 ); 3178 flush(); 3179 } 3180 } 3181 finally { 3182 if (!terminal.isSupported()) { 3183 afterReadLine(); 3184 } 3185 if (handleUserInterrupt && (terminal instanceof UnixTerminal)) { 3186 ((UnixTerminal) terminal).enableInterruptCharacter(); 3187 } 3188 } 3189 } 3190 3191 /** 3192 * Read a line for unsupported terminals. 3193 */ 3194 private String readLineSimple() throws IOException { 3195 StringBuilder buff = new StringBuilder(); 3196 3197 if (skipLF) { 3198 skipLF = false; 3199 3200 int i = readCharacter(); 3201 3202 if (i == -1 || i == '\r') { 3203 return buff.toString(); 3204 } else if (i == '\n') { 3205 // ignore 3206 } else { 3207 buff.append((char) i); 3208 } 3209 } 3210 3211 while (true) { 3212 int i = readCharacter(); 3213 3214 if (i == -1 && buff.length() == 0) { 3215 return null; 3216 } 3217 3218 if (i == -1 || i == '\n') { 3219 return buff.toString(); 3220 } else if (i == '\r') { 3221 skipLF = true; 3222 return buff.toString(); 3223 } else { 3224 buff.append((char) i); 3225 } 3226 } 3227 } 3228 3229 // 3230 // Completion 3231 // 3232 3233 private final List<Completer> completers = new LinkedList<Completer>(); 3234 3235 private CompletionHandler completionHandler = new CandidateListCompletionHandler(); 3236 3237 /** 3238 * Add the specified {@link jline.console.completer.Completer} to the list of handlers for tab-completion. 3239 * 3240 * @param completer the {@link jline.console.completer.Completer} to add 3241 * @return true if it was successfully added 3242 */ 3243 public boolean addCompleter(final Completer completer) { 3244 return completers.add(completer); 3245 } 3246 3247 /** 3248 * Remove the specified {@link jline.console.completer.Completer} from the list of handlers for tab-completion. 3249 * 3250 * @param completer The {@link Completer} to remove 3251 * @return True if it was successfully removed 3252 */ 3253 public boolean removeCompleter(final Completer completer) { 3254 return completers.remove(completer); 3255 } 3256 3257 /** 3258 * Returns an unmodifiable list of all the completers. 3259 */ 3260 public Collection<Completer> getCompleters() { 3261 return Collections.unmodifiableList(completers); 3262 } 3263 3264 public void setCompletionHandler(final CompletionHandler handler) { 3265 this.completionHandler = checkNotNull(handler); 3266 } 3267 3268 public CompletionHandler getCompletionHandler() { 3269 return this.completionHandler; 3270 } 3271 3272 /** 3273 * Use the completers to modify the buffer with the appropriate completions. 3274 * 3275 * @return true if successful 3276 */ 3277 protected boolean complete() throws IOException { 3278 // debug ("tab for (" + buf + ")"); 3279 if (completers.size() == 0) { 3280 return false; 3281 } 3282 3283 List<CharSequence> candidates = new LinkedList<CharSequence>(); 3284 String bufstr = buf.buffer.toString(); 3285 int cursor = buf.cursor; 3286 3287 int position = -1; 3288 3289 for (Completer comp : completers) { 3290 if ((position = comp.complete(bufstr, cursor, candidates)) != -1) { 3291 break; 3292 } 3293 } 3294 3295 return candidates.size() != 0 && getCompletionHandler().complete(this, candidates, position); 3296 } 3297 3298 protected void printCompletionCandidates() throws IOException { 3299 // debug ("tab for (" + buf + ")"); 3300 if (completers.size() == 0) { 3301 return; 3302 } 3303 3304 List<CharSequence> candidates = new LinkedList<CharSequence>(); 3305 String bufstr = buf.buffer.toString(); 3306 int cursor = buf.cursor; 3307 3308 for (Completer comp : completers) { 3309 if (comp.complete(bufstr, cursor, candidates) != -1) { 3310 break; 3311 } 3312 } 3313 CandidateListCompletionHandler.printCandidates(this, candidates); 3314 drawLine(); 3315 } 3316 3317 /** 3318 * The number of tab-completion candidates above which a warning will be 3319 * prompted before showing all the candidates. 3320 */ 3321 private int autoprintThreshold = Configuration.getInteger(JLINE_COMPLETION_THRESHOLD, 100); // same default as bash 3322 3323 /** 3324 * @param threshold the number of candidates to print without issuing a warning. 3325 */ 3326 public void setAutoprintThreshold(final int threshold) { 3327 this.autoprintThreshold = threshold; 3328 } 3329 3330 /** 3331 * @return the number of candidates to print without issuing a warning. 3332 */ 3333 public int getAutoprintThreshold() { 3334 return autoprintThreshold; 3335 } 3336 3337 private boolean paginationEnabled; 3338 3339 /** 3340 * Whether to use pagination when the number of rows of candidates exceeds the height of the terminal. 3341 */ 3342 public void setPaginationEnabled(final boolean enabled) { 3343 this.paginationEnabled = enabled; 3344 } 3345 3346 /** 3347 * Whether to use pagination when the number of rows of candidates exceeds the height of the terminal. 3348 */ 3349 public boolean isPaginationEnabled() { 3350 return paginationEnabled; 3351 } 3352 3353 // 3354 // History 3355 // 3356 3357 private History history = new MemoryHistory(); 3358 3359 public void setHistory(final History history) { 3360 this.history = history; 3361 } 3362 3363 public History getHistory() { 3364 return history; 3365 } 3366 3367 private boolean historyEnabled = true; 3368 3369 /** 3370 * Whether or not to add new commands to the history buffer. 3371 */ 3372 public void setHistoryEnabled(final boolean enabled) { 3373 this.historyEnabled = enabled; 3374 } 3375 3376 /** 3377 * Whether or not to add new commands to the history buffer. 3378 */ 3379 public boolean isHistoryEnabled() { 3380 return historyEnabled; 3381 } 3382 3383 /** 3384 * Used in "vi" mode for argumented history move, to move a specific 3385 * number of history entries forward or back. 3386 * 3387 * @param next If true, move forward 3388 * @param count The number of entries to move 3389 * @return true if the move was successful 3390 * @throws IOException 3391 */ 3392 private boolean moveHistory(final boolean next, int count) throws IOException { 3393 boolean ok = true; 3394 for (int i = 0; i < count && (ok = moveHistory(next)); i++) { 3395 /* empty */ 3396 } 3397 return ok; 3398 } 3399 3400 /** 3401 * Move up or down the history tree. 3402 */ 3403 private boolean moveHistory(final boolean next) throws IOException { 3404 if (next && !history.next()) { 3405 return false; 3406 } 3407 else if (!next && !history.previous()) { 3408 return false; 3409 } 3410 3411 setBuffer(history.current()); 3412 3413 return true; 3414 } 3415 3416 // 3417 // Printing 3418 // 3419 3420 public static final String CR = Configuration.getLineSeparator(); 3421 3422 /** 3423 * Output the specified character to the output stream without manipulating the current buffer. 3424 */ 3425 private void print(final int c) throws IOException { 3426 if (c == '\t') { 3427 char chars[] = new char[TAB_WIDTH]; 3428 Arrays.fill(chars, ' '); 3429 out.write(chars); 3430 return; 3431 } 3432 3433 out.write(c); 3434 } 3435 3436 /** 3437 * Output the specified characters to the output stream without manipulating the current buffer. 3438 */ 3439 private void print(final char... buff) throws IOException { 3440 int len = 0; 3441 for (char c : buff) { 3442 if (c == '\t') { 3443 len += TAB_WIDTH; 3444 } 3445 else { 3446 len++; 3447 } 3448 } 3449 3450 char chars[]; 3451 if (len == buff.length) { 3452 chars = buff; 3453 } 3454 else { 3455 chars = new char[len]; 3456 int pos = 0; 3457 for (char c : buff) { 3458 if (c == '\t') { 3459 Arrays.fill(chars, pos, pos + TAB_WIDTH, ' '); 3460 pos += TAB_WIDTH; 3461 } 3462 else { 3463 chars[pos] = c; 3464 pos++; 3465 } 3466 } 3467 } 3468 3469 out.write(chars); 3470 } 3471 3472 private void print(final char c, final int num) throws IOException { 3473 if (num == 1) { 3474 print(c); 3475 } 3476 else { 3477 char[] chars = new char[num]; 3478 Arrays.fill(chars, c); 3479 print(chars); 3480 } 3481 } 3482 3483 /** 3484 * Output the specified string to the output stream (but not the buffer). 3485 */ 3486 public final void print(final CharSequence s) throws IOException { 3487 print(checkNotNull(s).toString().toCharArray()); 3488 } 3489 3490 public final void println(final CharSequence s) throws IOException { 3491 print(checkNotNull(s).toString().toCharArray()); 3492 println(); 3493 } 3494 3495 /** 3496 * Output a platform-dependant newline. 3497 */ 3498 public final void println() throws IOException { 3499 print(CR); 3500 // flush(); 3501 } 3502 3503 // 3504 // Actions 3505 // 3506 3507 /** 3508 * Issue a delete. 3509 * 3510 * @return true if successful 3511 */ 3512 public final boolean delete() throws IOException { 3513 if (buf.cursor == buf.buffer.length()) { 3514 return false; 3515 } 3516 3517 buf.buffer.delete(buf.cursor, buf.cursor + 1); 3518 drawBuffer(1); 3519 3520 return true; 3521 } 3522 3523 /** 3524 * Kill the buffer ahead of the current cursor position. 3525 * 3526 * @return true if successful 3527 */ 3528 public boolean killLine() throws IOException { 3529 int cp = buf.cursor; 3530 int len = buf.buffer.length(); 3531 3532 if (cp >= len) { 3533 return false; 3534 } 3535 3536 int num = len - cp; 3537 clearAhead(num, 0); 3538 3539 char[] killed = new char[num]; 3540 buf.buffer.getChars(cp, (cp + num), killed, 0); 3541 buf.buffer.delete(cp, (cp + num)); 3542 3543 String copy = new String(killed); 3544 killRing.add(copy); 3545 3546 return true; 3547 } 3548 3549 public boolean yank() throws IOException { 3550 String yanked = killRing.yank(); 3551 3552 if (yanked == null) { 3553 return false; 3554 } 3555 putString(yanked); 3556 return true; 3557 } 3558 3559 public boolean yankPop() throws IOException { 3560 if (!killRing.lastYank()) { 3561 return false; 3562 } 3563 String current = killRing.yank(); 3564 if (current == null) { 3565 // This shouldn't happen. 3566 return false; 3567 } 3568 backspace(current.length()); 3569 String yanked = killRing.yankPop(); 3570 if (yanked == null) { 3571 // This shouldn't happen. 3572 return false; 3573 } 3574 3575 putString(yanked); 3576 return true; 3577 } 3578 3579 /** 3580 * Clear the screen by issuing the ANSI "clear screen" code. 3581 */ 3582 public boolean clearScreen() throws IOException { 3583 if (!terminal.isAnsiSupported()) { 3584 return false; 3585 } 3586 3587 // send the ANSI code to clear the screen 3588 printAnsiSequence("2J"); 3589 3590 // then send the ANSI code to go to position 1,1 3591 printAnsiSequence("1;1H"); 3592 3593 return true; 3594 } 3595 3596 /** 3597 * Issue an audible keyboard bell. 3598 */ 3599 public void beep() throws IOException { 3600 if (bellEnabled) { 3601 print(KEYBOARD_BELL); 3602 // need to flush so the console actually beeps 3603 flush(); 3604 } 3605 } 3606 3607 //disabled to avoid dependency on java.desktop: 3608 // /** 3609 // * Paste the contents of the clipboard into the console buffer 3610 // * 3611 // * @return true if clipboard contents pasted 3612 // */ 3613 // public boolean paste() throws IOException { 3614 // Clipboard clipboard; 3615 // try { // May throw ugly exception on system without X 3616 // clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); 3617 // } 3618 // catch (Exception e) { 3619 // return false; 3620 // } 3621 // 3622 // if (clipboard == null) { 3623 // return false; 3624 // } 3625 // 3626 // Transferable transferable = clipboard.getContents(null); 3627 // 3628 // if (transferable == null) { 3629 // return false; 3630 // } 3631 // 3632 // try { 3633 // @SuppressWarnings("deprecation") 3634 // Object content = transferable.getTransferData(DataFlavor.plainTextFlavor); 3635 // 3636 // // This fix was suggested in bug #1060649 at 3637 // // http://sourceforge.net/tracker/index.php?func=detail&aid=1060649&group_id=64033&atid=506056 3638 // // to get around the deprecated DataFlavor.plainTextFlavor, but it 3639 // // raises a UnsupportedFlavorException on Mac OS X 3640 // 3641 // if (content == null) { 3642 // try { 3643 // content = new DataFlavor().getReaderForText(transferable); 3644 // } 3645 // catch (Exception e) { 3646 // // ignore 3647 // } 3648 // } 3649 // 3650 // if (content == null) { 3651 // return false; 3652 // } 3653 // 3654 // String value; 3655 // 3656 // if (content instanceof Reader) { 3657 // // TODO: we might want instead connect to the input stream 3658 // // so we can interpret individual lines 3659 // value = ""; 3660 // String line; 3661 // 3662 // BufferedReader read = new BufferedReader((Reader) content); 3663 // while ((line = read.readLine()) != null) { 3664 // if (value.length() > 0) { 3665 // value += "\n"; 3666 // } 3667 // 3668 // value += line; 3669 // } 3670 // } 3671 // else { 3672 // value = content.toString(); 3673 // } 3674 // 3675 // if (value == null) { 3676 // return true; 3677 // } 3678 // 3679 // putString(value); 3680 // 3681 // return true; 3682 // } 3683 // catch (UnsupportedFlavorException e) { 3684 // Log.error("Paste failed: ", e); 3685 // 3686 // return false; 3687 // } 3688 // } 3689 3690 //disabled to avoid dependency on java.desktop: 3691 // // 3692 // // Triggered Actions 3693 // // 3694 // 3695 // private final Map<Character, ActionListener> triggeredActions = new HashMap<Character, ActionListener>(); 3696 // 3697 // /** 3698 // * Adding a triggered Action allows to give another curse of action if a character passed the pre-processing. 3699 // * <p/> 3700 // * Say you want to close the application if the user enter q. 3701 // * addTriggerAction('q', new ActionListener(){ System.exit(0); }); would do the trick. 3702 // */ 3703 // public void addTriggeredAction(final char c, final ActionListener listener) { 3704 // triggeredActions.put(c, listener); 3705 // } 3706 3707 // 3708 // Formatted Output 3709 // 3710 3711 /** 3712 * Output the specified {@link Collection} in proper columns. 3713 */ 3714 public void printColumns(final Collection<? extends CharSequence> items) throws IOException { 3715 if (items == null || items.isEmpty()) { 3716 return; 3717 } 3718 3719 int width = getTerminal().getWidth(); 3720 int height = getTerminal().getHeight(); 3721 3722 int maxWidth = 0; 3723 for (CharSequence item : items) { 3724 maxWidth = Math.max(maxWidth, item.length()); 3725 } 3726 maxWidth = maxWidth + 3; 3727 Log.debug("Max width: ", maxWidth); 3728 3729 int showLines; 3730 if (isPaginationEnabled()) { 3731 showLines = height - 1; // page limit 3732 } 3733 else { 3734 showLines = Integer.MAX_VALUE; 3735 } 3736 3737 StringBuilder buff = new StringBuilder(); 3738 for (CharSequence item : items) { 3739 if ((buff.length() + maxWidth) > width) { 3740 println(buff); 3741 buff.setLength(0); 3742 3743 if (--showLines == 0) { 3744 // Overflow 3745 print(resources.getString("DISPLAY_MORE")); 3746 flush(); 3747 int c = readCharacter(); 3748 if (c == '\r' || c == '\n') { 3749 // one step forward 3750 showLines = 1; 3751 } 3752 else if (c != 'q') { 3753 // page forward 3754 showLines = height - 1; 3755 } 3756 3757 back(resources.getString("DISPLAY_MORE").length()); 3758 if (c == 'q') { 3759 // cancel 3760 break; 3761 } 3762 } 3763 } 3764 3765 // NOTE: toString() is important here due to AnsiString being retarded 3766 buff.append(item.toString()); 3767 for (int i = 0; i < (maxWidth - item.length()); i++) { 3768 buff.append(' '); 3769 } 3770 } 3771 3772 if (buff.length() > 0) { 3773 println(buff); 3774 } 3775 } 3776 3777 // 3778 // Non-supported Terminal Support 3779 // 3780 3781 private Thread maskThread; 3782 3783 private void beforeReadLine(final String prompt, final Character mask) { 3784 if (mask != null && maskThread == null) { 3785 final String fullPrompt = "\r" + prompt 3786 + " " 3787 + " " 3788 + " " 3789 + "\r" + prompt; 3790 3791 maskThread = new Thread() 3792 { 3793 public void run() { 3794 while (!interrupted()) { 3795 try { 3796 Writer out = getOutput(); 3797 out.write(fullPrompt); 3798 out.flush(); 3799 sleep(3); 3800 } 3801 catch (IOException e) { 3802 return; 3803 } 3804 catch (InterruptedException e) { 3805 return; 3806 } 3807 } 3808 } 3809 }; 3810 3811 maskThread.setPriority(Thread.MAX_PRIORITY); 3812 maskThread.setDaemon(true); 3813 maskThread.start(); 3814 } 3815 } 3816 3817 private void afterReadLine() { 3818 if (maskThread != null && maskThread.isAlive()) { 3819 maskThread.interrupt(); 3820 } 3821 3822 maskThread = null; 3823 } 3824 3825 /** 3826 * Erases the current line with the existing prompt, then redraws the line 3827 * with the provided prompt and buffer 3828 * @param prompt 3829 * the new prompt 3830 * @param buffer 3831 * the buffer to be drawn 3832 * @param cursorDest 3833 * where you want the cursor set when the line has been drawn. 3834 * -1 for end of line. 3835 * */ 3836 public void resetPromptLine(String prompt, String buffer, int cursorDest) throws IOException { 3837 // move cursor to end of line 3838 moveToEnd(); 3839 3840 // backspace all text, including prompt 3841 buf.buffer.append(this.prompt); 3842 int promptLength = 0; 3843 if (this.prompt != null) { 3844 promptLength = this.prompt.length(); 3845 } 3846 3847 buf.cursor += promptLength; 3848 setPrompt(""); 3849 backspaceAll(); 3850 3851 setPrompt(prompt); 3852 redrawLine(); 3853 setBuffer(buffer); 3854 3855 // move cursor to destination (-1 will move to end of line) 3856 if (cursorDest < 0) cursorDest = buffer.length(); 3857 setCursorPosition(cursorDest); 3858 3859 flush(); 3860 } 3861 3862 public void printSearchStatus(String searchTerm, String match) throws IOException { 3863 printSearchStatus(searchTerm, match, "(reverse-i-search)`"); 3864 } 3865 3866 public void printForwardSearchStatus(String searchTerm, String match) throws IOException { 3867 printSearchStatus(searchTerm, match, "(i-search)`"); 3868 } 3869 3870 private void printSearchStatus(String searchTerm, String match, String searchLabel) throws IOException { 3871 String prompt = searchLabel + searchTerm + "': "; 3872 int cursorDest = match.indexOf(searchTerm); 3873 resetPromptLine(prompt, match, cursorDest); 3874 } 3875 3876 public void restoreLine(String originalPrompt, int cursorDest) throws IOException { 3877 // TODO move cursor to matched string 3878 String prompt = lastLine(originalPrompt); 3879 String buffer = buf.buffer.toString(); 3880 resetPromptLine(prompt, buffer, cursorDest); 3881 } 3882 3883 // 3884 // History search 3885 // 3886 /** 3887 * Search backward in history from a given position. 3888 * 3889 * @param searchTerm substring to search for. 3890 * @param startIndex the index from which on to search 3891 * @return index where this substring has been found, or -1 else. 3892 */ 3893 public int searchBackwards(String searchTerm, int startIndex) { 3894 return searchBackwards(searchTerm, startIndex, false); 3895 } 3896 3897 /** 3898 * Search backwards in history from the current position. 3899 * 3900 * @param searchTerm substring to search for. 3901 * @return index where the substring has been found, or -1 else. 3902 */ 3903 public int searchBackwards(String searchTerm) { 3904 return searchBackwards(searchTerm, history.index()); 3905 } 3906 3907 3908 public int searchBackwards(String searchTerm, int startIndex, boolean startsWith) { 3909 ListIterator<History.Entry> it = history.entries(startIndex); 3910 while (it.hasPrevious()) { 3911 History.Entry e = it.previous(); 3912 if (startsWith) { 3913 if (e.value().toString().startsWith(searchTerm)) { 3914 return e.index(); 3915 } 3916 } else { 3917 if (e.value().toString().contains(searchTerm)) { 3918 return e.index(); 3919 } 3920 } 3921 } 3922 return -1; 3923 } 3924 3925 /** 3926 * Search forward in history from a given position. 3927 * 3928 * @param searchTerm substring to search for. 3929 * @param startIndex the index from which on to search 3930 * @return index where this substring has been found, or -1 else. 3931 */ 3932 public int searchForwards(String searchTerm, int startIndex) { 3933 return searchForwards(searchTerm, startIndex, false); 3934 } 3935 /** 3936 * Search forwards in history from the current position. 3937 * 3938 * @param searchTerm substring to search for. 3939 * @return index where the substring has been found, or -1 else. 3940 */ 3941 public int searchForwards(String searchTerm) { 3942 return searchForwards(searchTerm, history.index()); 3943 } 3944 3945 public int searchForwards(String searchTerm, int startIndex, boolean startsWith) { 3946 if (startIndex >= history.size()) { 3947 startIndex = history.size() - 1; 3948 } 3949 3950 ListIterator<History.Entry> it = history.entries(startIndex); 3951 3952 if (searchIndex != -1 && it.hasNext()) { 3953 it.next(); 3954 } 3955 3956 while (it.hasNext()) { 3957 History.Entry e = it.next(); 3958 if (startsWith) { 3959 if (e.value().toString().startsWith(searchTerm)) { 3960 return e.index(); 3961 } 3962 } else { 3963 if (e.value().toString().contains(searchTerm)) { 3964 return e.index(); 3965 } 3966 } 3967 } 3968 return -1; 3969 } 3970 3971 // 3972 // Helpers 3973 // 3974 3975 /** 3976 * Checks to see if the specified character is a delimiter. We consider a 3977 * character a delimiter if it is anything but a letter or digit. 3978 * 3979 * @param c The character to test 3980 * @return True if it is a delimiter 3981 */ 3982 private boolean isDelimiter(final char c) { 3983 return !Character.isLetterOrDigit(c); 3984 } 3985 3986 /** 3987 * Checks to see if a character is a whitespace character. Currently 3988 * this delegates to {@link Character#isWhitespace(char)}, however 3989 * eventually it should be hooked up so that the definition of whitespace 3990 * can be configured, as readline does. 3991 * 3992 * @param c The character to check 3993 * @return true if the character is a whitespace 3994 */ 3995 private boolean isWhitespace(final char c) { 3996 return Character.isWhitespace (c); 3997 } 3998 3999 private void printAnsiSequence(String sequence) throws IOException { 4000 print(27); 4001 print('['); 4002 print(sequence); 4003 flush(); // helps with step debugging 4004 } 4005 4006 }