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 }