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.completer; 10 11 import jdk.internal.jline.console.ConsoleReader; 12 import jdk.internal.jline.console.CursorBuffer; 13 14 import java.io.IOException; 15 import java.util.ArrayList; 16 import java.util.Collection; 17 import java.util.HashSet; 18 import java.util.List; 19 import java.util.Locale; 20 import java.util.ResourceBundle; 21 import java.util.Set; 22 23 /** 24 * A {@link CompletionHandler} that deals with multiple distinct completions 25 * by outputting the complete list of possibilities to the console. This 26 * mimics the behavior of the 27 * <a href="http://www.gnu.org/directory/readline.html">readline</a> library. 28 * 29 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> 30 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a> 31 * @since 2.3 32 */ 33 public class CandidateListCompletionHandler 34 implements CompletionHandler 35 { 36 // TODO: handle quotes and escaped quotes && enable automatic escaping of whitespace 37 38 public boolean complete(final ConsoleReader reader, final List<CharSequence> candidates, final int pos) throws 39 IOException 40 { 41 CursorBuffer buf = reader.getCursorBuffer(); 42 43 // if there is only one completion, then fill in the buffer 44 if (candidates.size() == 1) { 45 CharSequence value = candidates.get(0); 46 47 // fail if the only candidate is the same as the current buffer 48 if (value.equals(buf.toString())) { 49 return false; 50 } 51 52 setBuffer(reader, value, pos); 53 54 return true; 55 } 56 else if (candidates.size() > 1) { 57 String value = getUnambiguousCompletions(candidates); 58 setBuffer(reader, value, pos); 59 } 60 61 printCandidates(reader, candidates); 62 63 // redraw the current console buffer 64 reader.drawLine(); 65 66 return true; 67 } 68 69 public static void setBuffer(final ConsoleReader reader, final CharSequence value, final int offset) throws 70 IOException 71 { 72 while ((reader.getCursorBuffer().cursor > offset) && reader.backspace()) { 73 // empty 74 } 75 76 reader.putString(value); 77 reader.setCursorPosition(offset + value.length()); 78 } 79 80 /** 81 * Print out the candidates. If the size of the candidates is greater than the 82 * {@link ConsoleReader#getAutoprintThreshold}, they prompt with a warning. 83 * 84 * @param candidates the list of candidates to print 85 */ 86 public static void printCandidates(final ConsoleReader reader, Collection<CharSequence> candidates) throws 87 IOException 88 { 89 Set<CharSequence> distinct = new HashSet<CharSequence>(candidates); 90 91 if (distinct.size() > reader.getAutoprintThreshold()) { 92 //noinspection StringConcatenation 93 reader.print(Messages.DISPLAY_CANDIDATES.format(candidates.size())); 94 reader.flush(); 95 96 int c; 97 98 String noOpt = Messages.DISPLAY_CANDIDATES_NO.format(); 99 String yesOpt = Messages.DISPLAY_CANDIDATES_YES.format(); 100 char[] allowed = {yesOpt.charAt(0), noOpt.charAt(0)}; 101 102 while ((c = reader.readCharacter(allowed)) != -1) { 103 String tmp = new String(new char[]{(char) c}); 104 105 if (noOpt.startsWith(tmp)) { 106 reader.println(); 107 return; 108 } 109 else if (yesOpt.startsWith(tmp)) { 110 break; 111 } 112 else { 113 reader.beep(); 114 } 115 } 116 } 117 118 // copy the values and make them distinct, without otherwise affecting the ordering. Only do it if the sizes differ. 119 if (distinct.size() != candidates.size()) { 120 Collection<CharSequence> copy = new ArrayList<CharSequence>(); 121 122 for (CharSequence next : candidates) { 123 if (!copy.contains(next)) { 124 copy.add(next); 125 } 126 } 127 128 candidates = copy; 129 } 130 131 reader.println(); 132 reader.printColumns(candidates); 133 } 134 135 /** 136 * Returns a root that matches all the {@link String} elements of the specified {@link List}, 137 * or null if there are no commonalities. For example, if the list contains 138 * <i>foobar</i>, <i>foobaz</i>, <i>foobuz</i>, the method will return <i>foob</i>. 139 */ 140 private String getUnambiguousCompletions(final List<CharSequence> candidates) { 141 if (candidates == null || candidates.isEmpty()) { 142 return null; 143 } 144 145 // convert to an array for speed 146 String[] strings = candidates.toArray(new String[candidates.size()]); 147 148 String first = strings[0]; 149 StringBuilder candidate = new StringBuilder(); 150 151 for (int i = 0; i < first.length(); i++) { 152 if (startsWith(first.substring(0, i + 1), strings)) { 153 candidate.append(first.charAt(i)); 154 } 155 else { 156 break; 157 } 158 } 159 160 return candidate.toString(); 161 } 162 163 /** 164 * @return true is all the elements of <i>candidates</i> start with <i>starts</i> 165 */ 166 private boolean startsWith(final String starts, final String[] candidates) { 167 for (String candidate : candidates) { 168 if (!candidate.startsWith(starts)) { 169 return false; 170 } 171 } 172 173 return true; 174 } 175 176 private static enum Messages 177 { 178 DISPLAY_CANDIDATES, 179 DISPLAY_CANDIDATES_YES, 180 DISPLAY_CANDIDATES_NO,; 181 182 private static final 183 ResourceBundle 184 bundle = 185 ResourceBundle.getBundle(CandidateListCompletionHandler.class.getName(), Locale.getDefault()); 186 187 public String format(final Object... args) { 188 if (bundle == null) 189 return ""; 190 else 191 return String.format(bundle.getString(name()), args); 192 } 193 } 194 }