/* * Licensed to Luca Cavanna (the "Author") under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. Elastic Search licenses this * file to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.elasticsearch.shell.console.completer; import jline.console.ConsoleReader; import jline.console.CursorBuffer; import jline.console.completer.CandidateListCompletionHandler; import jline.console.completer.CompletionHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.*; /** * JLine {@link CompletionHandler} that displays auto-suggestions * @see CandidateListCompletionHandler * * @author Luca Cavanna */ public class JLineCompletionHandler implements CompletionHandler { private static final Logger logger = LoggerFactory.getLogger(JLineCompletionHandler.class); public boolean complete(final ConsoleReader reader, final List<CharSequence> candidates, final int pos) throws IOException { CursorBuffer buffer = reader.getCursorBuffer(); // if there is only one completion, then fill in the buffer, that's all if (candidates.size() == 1) { CharSequence candidate = candidates.get(0); // fail if the only candidate is the same as the current buffer if (candidate.equals(buffer.toString())) { return false; } updateBuffer(reader, candidate, pos); return true; } //if there are more suggestions, then fill in the buffer with the longer common prefix available //and show auto-suggestions if (candidates.size() > 1) { String commonPrefix = getUnambiguousCompletions(candidates); updateBuffer(reader, commonPrefix, pos); } printCandidates(reader, candidates); //next line seems to cause small problems when cursor is not at the end of the buffer //e.g. FilterBuilders.queryFilter(QueryBuilders.) reader.drawLine(); return true; } /** * Prints out the candidates. If the size of the candidates is greater than the * {@link ConsoleReader#getAutoprintThreshold}, a warning is printed * * @param candidates the list of candidates to print */ protected void printCandidates(final ConsoleReader reader, List<CharSequence> candidates) throws IOException { Set<CharSequence> distinctCandidates = new HashSet<CharSequence>(candidates); if (distinctCandidates.size() > reader.getAutoprintThreshold()) { reader.println(); reader.print(Messages.DISPLAY_CANDIDATES.format(candidates.size())); reader.flush(); char[] allowed = {'y', 'n'}; int c; while ((c = reader.readCharacter(allowed)) != -1) { if (c=='n') { reader.println(); return; } if (c=='y') { break; } reader.beep(); } } reader.println(); reader.printColumns(sortCandidates(distinctCandidates)); } protected List<CharSequence> sortCandidates(Set<CharSequence> candidates) { List<CharSequence> orderedCandidates = new ArrayList<CharSequence>(candidates); Collections.sort(orderedCandidates, new Comparator<CharSequence>() { @Override public int compare(CharSequence o1, CharSequence o2) { return o1.toString().compareToIgnoreCase(o2.toString()); } }); return orderedCandidates; } protected void updateBuffer(final ConsoleReader reader, final CharSequence output, final int offset) throws IOException { while ((reader.getCursorBuffer().cursor > offset) && reader.backspace()) { } if (output != null && output.length() > 0) { reader.putString(output); int newCursorPosition = offset + output.length(); if (output.charAt(output.length()-1) == ')' && output.charAt(output.length()-2) == '(' ) { //we want to put the cursor between the parentheses here newCursorPosition--; } reader.setCursorPosition(newCursorPosition); } } /** * Returns a root that matches all the {@link String} elements of the specified {@link List}, * or null if there are no commonalities. For example, if the list contains * <i>foobar</i>, <i>foobaz</i>, <i>foobuz</i>, the method will return <i>foob</i>. */ protected String getUnambiguousCompletions(final List<CharSequence> candidates) { if (candidates == null || candidates.isEmpty()) { return null; } CharSequence firstCandidate = candidates.get(0); StringBuilder commonPrefix = new StringBuilder(); for (int i = 0; i < firstCandidate.length(); i++) { char c = firstCandidate.charAt(i); StringBuilder tmpPrefix = new StringBuilder(commonPrefix).append(c); if (!allCandidatesStartsWith(candidates, tmpPrefix.toString())) { break; } commonPrefix = tmpPrefix; } return commonPrefix.toString(); } /** * @return true is all the elements of <i>candidates</i> start with <i>starts</i> */ protected boolean allCandidatesStartsWith(final List<CharSequence> candidates, final String commonPrefix) { for (CharSequence candidate : candidates) { if (!candidate.toString().startsWith(commonPrefix)) { return false; } } return true; } private static enum Messages { DISPLAY_CANDIDATES; private static final ResourceBundle bundle = ResourceBundle.getBundle(CandidateListCompletionHandler.class.getName(), Locale.getDefault()); public String format(final Object... args) { if (bundle == null) { return ""; } return String.format(bundle.getString(name()), args); } } }