001/*
002 * (C) Copyright 2006-2010 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     bstefanescu
016 */
017package org.nuxeo.shell.swing;
018
019import java.io.IOException;
020import java.util.ArrayList;
021import java.util.HashSet;
022import java.util.Iterator;
023import java.util.List;
024import java.util.Set;
025
026import javax.swing.JOptionPane;
027
028import jline.CompletionHandler;
029import jline.ConsoleReader;
030import jline.CursorBuffer;
031
032import org.nuxeo.shell.Shell;
033
034/**
035 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
036 */
037@SuppressWarnings({ "unchecked", "rawtypes" })
038public class SwingCompletionHandler implements CompletionHandler {
039
040    protected Console console;
041
042    public SwingCompletionHandler(Console console) {
043        this.console = console;
044    }
045
046    public boolean complete(ConsoleReader reader, List candidates, int position) throws IOException {
047        CursorBuffer buf = reader.getCursorBuffer();
048
049        // if there is only one completion, then fill in the buffer
050        if (candidates.size() == 1) {
051            String value = candidates.get(0).toString();
052
053            // fail if the only candidate is the same as the current buffer
054            if (value.equals(buf.toString())) {
055                console.beep();
056                return false;
057            }
058
059            position = console.getCmdLine().setCompletionWord(value);
060            console.setCaretPosition(console.getCmdLine().getCmdStart() + position);
061            return true;
062        } else if (candidates.size() > 1) {
063            String value = getUnambiguousCompletions(candidates);
064            position = console.getCmdLine().setCompletionWord(value);
065        } else if (candidates.isEmpty()) {
066            console.beep();
067            return false;
068        }
069        String text = console.getCmdLine().getText();
070        console.append("\n");
071        printCandidates(candidates);
072        console.append("\n");
073        console.append(Shell.get().getActiveRegistry().getPrompt(Shell.get()));
074        console.cline = null;
075        console.getCmdLine().setText(text);
076        console.setCaretPosition(console.getCmdLine().getCmdStart() + position);
077
078        return true;
079    }
080
081    /**
082     * Returns a root that matches all the {@link String} elements of the specified {@link List}, or null if there are
083     * no commalities. For example, if the list contains <i>foobar</i>, <i>foobaz</i>, <i>foobuz</i>, the method will
084     * return <i>foob</i>.
085     */
086    private final String getUnambiguousCompletions(final List candidates) {
087        if ((candidates == null) || (candidates.size() == 0)) {
088            return null;
089        }
090
091        // convert to an array for speed
092        String[] strings = (String[]) candidates.toArray(new String[candidates.size()]);
093
094        String first = strings[0];
095        StringBuffer candidate = new StringBuffer();
096
097        for (int i = 0; i < first.length(); i++) {
098            if (startsWith(first.substring(0, i + 1), strings)) {
099                candidate.append(first.charAt(i));
100            } else {
101                break;
102            }
103        }
104
105        return candidate.toString();
106    }
107
108    /**
109     * @return true is all the elements of <i>candidates</i> start with <i>starts</i>
110     */
111    private final boolean startsWith(final String starts, final String[] candidates) {
112        for (int i = 0; i < candidates.length; i++) {
113            if (!candidates[i].startsWith(starts)) {
114                return false;
115            }
116        }
117
118        return true;
119    }
120
121    protected void printCandidates(List<String> candidates) {
122        Set<String> distinct = new HashSet<String>(candidates);
123
124        if (distinct.size() > console.reader.getAutoprintThreshhold()) {
125            // if (!eagerNewlines)
126            if (JOptionPane.showConfirmDialog(console, "Display all " + distinct.size() + " possibilities? (y or n) ",
127                    "Completion Warning", JOptionPane.YES_NO_OPTION) == 1) {
128                return;
129            }
130        }
131
132        // copy the values and make them distinct, without otherwise
133        // affecting the ordering. Only do it if the sizes differ.
134        if (distinct.size() != candidates.size()) {
135            List<String> copy = new ArrayList<String>();
136
137            for (Iterator<String> i = candidates.iterator(); i.hasNext();) {
138                String next = i.next();
139
140                if (!(copy.contains(next))) {
141                    copy.add(next);
142                }
143            }
144
145            candidates = copy;
146        }
147
148        console.printColumns(candidates);
149
150    }
151}