001    /*
002     * Copyright (c) 2002-2006, 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        public boolean complete(final ConsoleReader reader, final List candidates,
035                                final int pos) throws IOException {
036            CursorBuffer buf = reader.getCursorBuffer();
037    
038            // if there is only one completion, then fill in the buffer
039            if (candidates.size() == 1) {
040                String value = candidates.get(0).toString();
041    
042                // fail if the only candidate is the same as the current buffer
043                if (value.equals(buf.toString())) {
044                    return false;
045                }
046    
047                setBuffer(reader, value, pos);
048    
049                return true;
050            } else if (candidates.size() > 1) {
051                String value = getUnambiguousCompletions(candidates);
052                String bufString = buf.toString();
053                setBuffer(reader, value, pos);
054    
055                if (bufString.length() - pos != value.length())
056                    return true;
057            }
058    
059            reader.printNewline();
060            printCandidates(reader, candidates);
061    
062            // redraw the current console buffer
063            reader.drawLine();
064    
065            return true;
066        }
067    
068        private static void setBuffer(ConsoleReader reader, String value, int offset)
069                               throws IOException {
070            while ((reader.getCursorBuffer().cursor > offset)
071                       && reader.backspace()) {
072                ;
073            }
074    
075            reader.putString(value);
076            reader.setCursorPosition(offset + value.length());
077        }
078    
079        /**
080         *  Print out the candidates. If the size of the candidates
081         *  is greated than the {@link getAutoprintThreshhold},
082         *  they prompt with aq warning.
083         *
084         *  @param  candidates  the list of candidates to print
085         */
086        private final void printCandidates(ConsoleReader reader,
087                                           Collection candidates)
088                                    throws IOException {
089            Set distinct = new HashSet(candidates);
090    
091            if (distinct.size() > reader.getAutoprintThreshhold()) {
092                reader.printString(MessageFormat.format
093                    (loc.getString("display-candidates"), new Object[] {
094                        new Integer(candidates .size())
095                        }) + " ");
096    
097                reader.flushConsole();
098    
099                int c;
100    
101                String noOpt = loc.getString("display-candidates-no");
102                String yesOpt = loc.getString("display-candidates-yes");
103    
104                while ((c = reader.readCharacter(new char[] {
105                    yesOpt.charAt(0), noOpt.charAt(0) })) != -1) {
106                    if (noOpt.startsWith
107                        (new String(new char[] { (char) c }))) {
108                        reader.printNewline();
109                        return;
110                    } else if (yesOpt.startsWith
111                        (new String(new char[] { (char) c }))) {
112                        break;
113                    } else {
114                        reader.beep();
115                    }
116                }
117            }
118    
119            // copy the values and make them distinct, without otherwise
120            // affecting the ordering. Only do it if the sizes differ.
121            if (distinct.size() != candidates.size()) {
122                Collection copy = new ArrayList();
123    
124                for (Iterator i = candidates.iterator(); i.hasNext();) {
125                    Object next = i.next();
126    
127                    if (!(copy.contains(next))) {
128                        copy.add(next);
129                    }
130                }
131    
132                candidates = copy;
133            }
134    
135            reader.printNewline();
136            reader.printColumns(candidates);
137        }
138    
139        /**
140         *  Returns a root that matches all the {@link String} elements
141         *  of the specified {@link List}, or null if there are
142         *  no commalities. For example, if the list contains
143         *  <i>foobar</i>, <i>foobaz</i>, <i>foobuz</i>, the
144         *  method will return <i>foob</i>.
145         */
146        private final String getUnambiguousCompletions(final List candidates) {
147            if ((candidates == null) || (candidates.size() == 0)) {
148                return null;
149            }
150    
151            // convert to an array for speed
152            String[] strings =
153                (String[]) candidates.toArray(new String[candidates.size()]);
154    
155            String first = strings[0];
156            StringBuffer candidate = new StringBuffer();
157    
158            for (int i = 0; i < first.length(); i++) {
159                if (startsWith(first.substring(0, i + 1), strings)) {
160                    candidate.append(first.charAt(i));
161                } else {
162                    break;
163                }
164            }
165    
166            return candidate.toString();
167        }
168    
169        /**
170         *  @return  true is all the elements of <i>candidates</i>
171         *                          start with <i>starts</i>
172         */
173        private final boolean startsWith(final String starts,
174                                         final String[] candidates) {
175            for (int i = 0; i < candidates.length; i++) {
176                if (!candidates[i].startsWith(starts)) {
177                    return false;
178                }
179            }
180    
181            return true;
182        }
183    }