001 /*
002 * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved.
003 *
004 * This software is distributable under the BSD license. See the terms of the
005 * BSD license in the documentation provided with this software.
006 */
007 package jline;
008
009 import java.io.*;
010 import java.text.MessageFormat;
011 import java.util.*;
012
013 /**
014 * <p>
015 * A {@link CompletionHandler} that deals with multiple distinct completions
016 * by outputting the complete list of possibilities to the console. This
017 * mimics the behavior of the
018 * <a href="http://www.gnu.org/directory/readline.html">readline</a>
019 * library.
020 * </p>
021 *
022 * <strong>TODO:</strong>
023 * <ul>
024 * <li>handle quotes and escaped quotes</li>
025 * <li>enable automatic escaping of whitespace</li>
026 * </ul>
027 *
028 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
029 */
030 public class CandidateListCompletionHandler implements CompletionHandler {
031 private static ResourceBundle loc = ResourceBundle.
032 getBundle(CandidateListCompletionHandler.class.getName());
033
034 private boolean eagerNewlines = true;
035
036 public void setAlwaysIncludeNewline(boolean eagerNewlines) {
037 this.eagerNewlines = eagerNewlines;
038 }
039
040 public boolean complete(final ConsoleReader reader, final List candidates,
041 final int pos) throws IOException {
042 CursorBuffer buf = reader.getCursorBuffer();
043
044 // if there is only one completion, then fill in the buffer
045 if (candidates.size() == 1) {
046 String value = candidates.get(0).toString();
047
048 // fail if the only candidate is the same as the current buffer
049 if (value.equals(buf.toString())) {
050 return false;
051 }
052
053 setBuffer(reader, value, pos);
054
055 return true;
056 } else if (candidates.size() > 1) {
057 String value = getUnambiguousCompletions(candidates);
058 String bufString = buf.toString();
059 setBuffer(reader, value, pos);
060 }
061
062 if (eagerNewlines)
063 reader.printNewline();
064 printCandidates(reader, candidates, eagerNewlines);
065
066 // redraw the current console buffer
067 reader.drawLine();
068
069 return true;
070 }
071
072 public static void setBuffer(ConsoleReader reader, String value, int offset)
073 throws IOException {
074 while ((reader.getCursorBuffer().cursor > offset)
075 && reader.backspace()) {
076 ;
077 }
078
079 reader.putString(value);
080 reader.setCursorPosition(offset + value.length());
081 }
082
083 /**
084 * Print out the candidates. If the size of the candidates
085 * is greated than the {@link getAutoprintThreshhold},
086 * they prompt with aq warning.
087 *
088 * @param candidates the list of candidates to print
089 */
090 public static final void printCandidates(ConsoleReader reader,
091 Collection candidates, boolean eagerNewlines)
092 throws IOException {
093 Set distinct = new HashSet(candidates);
094
095 if (distinct.size() > reader.getAutoprintThreshhold()) {
096 if (!eagerNewlines)
097 reader.printNewline();
098 reader.printString(MessageFormat.format
099 (loc.getString("display-candidates"), new Object[] {
100 new Integer(candidates .size())
101 }) + " ");
102
103 reader.flushConsole();
104
105 int c;
106
107 String noOpt = loc.getString("display-candidates-no");
108 String yesOpt = loc.getString("display-candidates-yes");
109
110 while ((c = reader.readCharacter(new char[] {
111 yesOpt.charAt(0), noOpt.charAt(0) })) != -1) {
112 if (noOpt.startsWith
113 (new String(new char[] { (char) c }))) {
114 reader.printNewline();
115 return;
116 } else if (yesOpt.startsWith
117 (new String(new char[] { (char) c }))) {
118 break;
119 } else {
120 reader.beep();
121 }
122 }
123 }
124
125 // copy the values and make them distinct, without otherwise
126 // affecting the ordering. Only do it if the sizes differ.
127 if (distinct.size() != candidates.size()) {
128 Collection copy = new ArrayList();
129
130 for (Iterator i = candidates.iterator(); i.hasNext();) {
131 Object next = i.next();
132
133 if (!(copy.contains(next))) {
134 copy.add(next);
135 }
136 }
137
138 candidates = copy;
139 }
140
141 reader.printNewline();
142 reader.printColumns(candidates);
143 }
144
145 /**
146 * Returns a root that matches all the {@link String} elements
147 * of the specified {@link List}, or null if there are
148 * no commalities. For example, if the list contains
149 * <i>foobar</i>, <i>foobaz</i>, <i>foobuz</i>, the
150 * method will return <i>foob</i>.
151 */
152 private final String getUnambiguousCompletions(final List candidates) {
153 if ((candidates == null) || (candidates.size() == 0)) {
154 return null;
155 }
156
157 // convert to an array for speed
158 String[] strings =
159 (String[]) candidates.toArray(new String[candidates.size()]);
160
161 String first = strings[0];
162 StringBuffer candidate = new StringBuffer();
163
164 for (int i = 0; i < first.length(); i++) {
165 if (startsWith(first.substring(0, i + 1), strings)) {
166 candidate.append(first.charAt(i));
167 } else {
168 break;
169 }
170 }
171
172 return candidate.toString();
173 }
174
175 /**
176 * @return true is all the elements of <i>candidates</i>
177 * start with <i>starts</i>
178 */
179 private final boolean startsWith(final String starts,
180 final String[] candidates) {
181 for (int i = 0; i < candidates.length; i++) {
182 if (!candidates[i].startsWith(starts)) {
183 return false;
184 }
185 }
186
187 return true;
188 }
189 }