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