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 */
9package jdk.internal.jline.console.completer;
10
11import jdk.internal.jline.console.ConsoleReader;
12import jdk.internal.jline.console.CursorBuffer;
13
14import java.io.IOException;
15import java.util.ArrayList;
16import java.util.Collection;
17import java.util.HashSet;
18import java.util.List;
19import java.util.Locale;
20import java.util.ResourceBundle;
21import 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 */
33public 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}
195