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 */
007package jline;
008
009import java.io.*;
010import java.text.MessageFormat;
011import 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 */
030public 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}