View Javadoc

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 jline.console.completer;
10  
11  import jline.console.ConsoleReader;
12  import 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 }