blob: bb8ebd44f636c2c83ab107fc45b4beae936eea09 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010, 2017 SAP AG and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Lazar Kirchev, SAP AG - initial API and implementation
*******************************************************************************/
package org.eclipse.equinox.console.common;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;
import org.apache.felix.service.command.CommandSession;
import org.eclipse.equinox.console.completion.CompletionHandler;
import org.osgi.framework.BundleContext;
/**
* This class performs the processing of the input special characters,
* and updates respectively what is displayed in the output. It handles
* escape sequences, delete, backspace, arrows, insert, home, end, pageup, pagedown, tab completion.
*/
public class ConsoleInputScanner extends Scanner {
private static final byte TAB = 9;
private boolean isCR = false;
private boolean replace = false;
private boolean isCompletionMode = false;
// shows if command history should be saved - it is turned off in cases when passwords are to be entered
private boolean isHistoryEnabled = true;
private final HistoryHolder history;
private final SimpleByteBuffer buffer;
private CommandSession session;
private BundleContext context;
private Candidates candidates;
private int originalCursorPos;
public ConsoleInputScanner(ConsoleInputStream toShell, OutputStream toTelnet) {
super(toShell, toTelnet);
history = new HistoryHolder();
buffer = new SimpleByteBuffer();
}
public void toggleHistoryEnabled(boolean isEnabled) {
isHistoryEnabled = isEnabled;
}
public void setSession(CommandSession session) {
this.session = session;
}
public void setContext(BundleContext context) {
this.context = context;
}
@Override
public void scan(int b) throws IOException {
b &= 0xFF;
if (isCR) {
isCR = false;
if (b == LF) {
return;
}
}
if (b != TAB) {
if (isCompletionMode == true) {
isCompletionMode = false;
candidates = null;
originalCursorPos = 0;
}
}
if (isEsc) {
scanEsc(b);
} else {
if (b == getBackspace()) {
backSpace();
} else if(b == TAB) {
if (isCompletionMode == false) {
isCompletionMode = true;
processTab();
} else {
processNextTab();
}
} else if (b == CR) {
isCR = true;
processData();
} else if (b == LF) {
processData();
} else if (b == ESC) {
startEsc();
} else if (b == getDel()) {
delete();
} else {
if (b >= SPACE && b < MAX_CHAR) {
newChar(b);
}
}
}
}
private void delete() throws IOException {
clearLine();
buffer.delete();
echoBuff();
flush();
}
private void backSpace() throws IOException {
clearLine();
buffer.backSpace();
echoBuff();
flush();
}
protected void clearLine() throws IOException {
int size = buffer.getSize();
int pos = buffer.getPos();
for (int i = size - pos; i < size; i++) {
echo(BS);
}
for (int i = 0; i < size; i++) {
echo(SPACE);
}
for (int i = 0; i < size; i++) {
echo(BS);
}
}
protected void echoBuff() throws IOException {
byte[] data = buffer.copyCurrentData();
for (byte b : data) {
echo(b);
}
int pos = buffer.getPos();
for (int i = data.length; i > pos; i--) {
echo(BS);
}
}
protected void newChar(int b) throws IOException {
if (buffer.getPos() < buffer.getSize()) {
if (replace) {
buffer.replace(b);
} else {
buffer.insert(b);
}
clearLine();
echoBuff();
flush();
} else {
if (replace) {
buffer.replace(b);
} else {
buffer.insert(b);
}
}
}
protected void processTab() throws IOException {
CompletionHandler completionHandler = new CompletionHandler(context, session);
Map<String, Integer> completionCandidates = completionHandler.getCandidates(buffer.copyCurrentData(), buffer.getPos());
if (completionCandidates.size() == 1) {
completeSingleCandidate(completionCandidates);
isCompletionMode = false;
return;
}
printNewLine();
if (completionCandidates.size() == 0) {
printCompletionError();
isCompletionMode = false;
} else {
processCandidates(completionCandidates);
}
printNewLine();
printPrompt();
}
protected void processCandidates(Map<String, Integer> completionCandidates) throws IOException{
Set<String> candidatesNamesSet = completionCandidates.keySet();
String[] candidatesNames = (candidatesNamesSet.toArray(new String[0]));
originalCursorPos = buffer.getPos();
String[] candidateSuffixes = new String[candidatesNames.length];
for (int i = 0; i < candidatesNames.length; i++) {
String candidateName = candidatesNames[i];
candidateSuffixes[i] = getCandidateSuffix(candidateName, completionCandidates.get(candidateName), originalCursorPos);
for (byte symbol : candidateName.getBytes()) {
echo(symbol);
}
printNewLine();
}
String commonPrefix = getCommonPrefix(candidateSuffixes);
candidates = new Candidates(removeCommonPrefix(candidateSuffixes, commonPrefix));
printString(commonPrefix, false);
originalCursorPos = buffer.getPos();
}
protected void processNextTab() throws IOException {
if (candidates == null) {
return;
}
while (originalCursorPos < buffer.getPos()) {
backSpace();
}
String candidate = candidates.getCurrent();
if(!candidate.equals("")) {
printString(candidate, true);
}
}
protected void printCandidate(String candidate, int startIndex, int completionIndex) throws IOException {
String suffix = getCandidateSuffix(candidate, startIndex, completionIndex);
if(suffix.equals("")) {
return;
}
printString(suffix, true);
}
protected void printString(String st, boolean isEcho) throws IOException {
for (byte symbol : st.getBytes()) {
buffer.insert(symbol);
if (isEcho){
echo(symbol);
}
}
flush();
}
protected String getCommonPrefix(String[] names) {
if (names.length == 0) {
return "";
}
if (names.length == 1) {
return names[0];
}
StringBuilder builder = new StringBuilder();
char[] name = names[0].toCharArray();
for(char c : name) {
String prefix = builder.append(c).toString();
for (int i = 1; i < names.length; i ++) {
if (!names[i].startsWith(prefix)) {
return prefix.substring(0, prefix.length() - 1);
}
}
}
return builder.toString();
}
protected String[] removeCommonPrefix(String [] names, String commonPrefix){
ArrayList<String> result = new ArrayList<>();
for (String name : names) {
String nameWithoutPrefix = name.substring(commonPrefix.length());
if (nameWithoutPrefix.length() > 0) {
result.add(nameWithoutPrefix);
}
}
result.add("");
return result.toArray(new String[0]);
}
protected String getCandidateSuffix(String candidate, int startIndex, int completionIndex) {
int partialLength = completionIndex - startIndex;
if (partialLength >= candidate.length()) {
return "";
}
return candidate.substring(partialLength);
}
protected void completeSingleCandidate(Map<String, Integer> completionCandidates) throws IOException {
Set<String> keys = completionCandidates.keySet();
String key = (keys.toArray(new String[0]))[0];
int startIndex = completionCandidates.get(key);
printCandidate(key, startIndex, buffer.getPos());
}
protected void printCompletionError() throws IOException {
byte[] curr = buffer.getCurrentData();
if (isHistoryEnabled == true) {
history.add(curr);
}
String errorMessage = "No completion available";
for (byte symbol : errorMessage.getBytes()) {
echo(symbol);
}
}
protected void printNewLine() throws IOException{
echo(CR);
echo(LF);
flush();
}
protected void printPrompt() throws IOException{
echo('o');
echo('s');
echo('g');
echo('i');
echo('>');
echo(SPACE);
echoBuff();
flush();
}
private void processData() throws IOException {
// buffer.add(CR);
buffer.add(LF);
echo(CR);
echo(LF);
flush();
byte[] curr = buffer.getCurrentData();
if (isHistoryEnabled == true) {
history.add(curr);
}
toShell.add(curr);
}
public void resetHistory() {
history.reset();
}
@Override
protected void scanEsc(final int b) throws IOException {
esc += (char) b;
KEYS key = checkEscape(esc);
if (key == KEYS.UNFINISHED) {
return;
}
if (key == KEYS.UNKNOWN) {
isEsc = false;
scan(b);
return;
}
isEsc = false;
switch (key) {
case UP:
processUpArrow();
break;
case DOWN:
processDownArrow();
break;
case RIGHT:
processRightArrow();
break;
case LEFT:
processLeftArrow();
break;
case HOME:
processHome();
break;
case END:
processEnd();
break;
case PGUP:
processPgUp();
break;
case PGDN:
processPgDn();
break;
case INS:
processIns();
break;
case DEL:
delete();
break;
default: //CENTER
break;
}
}
private static final byte[] INVERSE_ON = {ESC, '[', '7', 'm'};
private static final byte[] INVERSE_OFF = {ESC, '[', '2', '7', 'm'};
private void echo(byte[] data) throws IOException {
for (byte b : data) {
echo(b);
}
}
private void processIns() throws IOException {
replace = !replace;
int b = buffer.getCurrentChar();
echo(INVERSE_ON);
echo(replace ? 'R' : 'I');
flush();
try {
Thread.sleep(300);
} catch (InterruptedException e) {
//do not care $JL-EXC$
}
echo(INVERSE_OFF);
echo(BS);
echo(b == -1 ? SPACE : b);
echo(BS);
flush();
}
private void processPgDn() throws IOException {
byte[] last = history.last();
if (last != null) {
clearLine();
buffer.set(last);
echoBuff();
flush();
}
}
private void processPgUp() throws IOException {
byte[] first = history.first();
if (first != null) {
clearLine();
buffer.set(first);
echoBuff();
flush();
}
}
private void processHome() throws IOException {
int pos = buffer.resetPos();
if (pos > 0) {
for (int i = 0; i < pos; i++) {
echo(BS);
}
flush();
}
}
private void processEnd() throws IOException {
int b;
while ((b = buffer.goRight()) != -1) {
echo(b);
}
flush();
}
private void processLeftArrow() throws IOException {
if (buffer.goLeft()) {
echo(BS);
flush();
}
}
private void processRightArrow() throws IOException {
int b = buffer.goRight();
if (b != -1) {
echo(b);
flush();
}
}
private void processDownArrow() throws IOException {
byte[] next = history.next();
if (next != null) {
clearLine();
buffer.set(next);
echoBuff();
flush();
}
}
private void processUpArrow() throws IOException {
clearLine();
byte[] prev = history.prev();
buffer.set(prev);
echoBuff();
flush();
}
private static class Candidates {
private String[] candidates;
private int currentCandidateIndex = 0;
public Candidates(String[] candidates) {
this.candidates = candidates.clone();
}
public String getCurrent() {
if (currentCandidateIndex >= candidates.length) {
currentCandidateIndex = 0;
}
return candidates[currentCandidateIndex++];
}
}
}