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    }