Skip to content

Commit

Permalink
Refactor and document cycle
Browse files Browse the repository at this point in the history
  • Loading branch information
stefan-kolb committed Apr 3, 2016
1 parent 3f81d9b commit a4c5c0f
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ public class AutoCompleteListener extends KeyAdapter implements FocusListener {
// null indicates that there are no completions available
private String textToInsert;
// the letters, the user has typed until know
private String lastBeginning;
private String currentWord;
private int lastCaretPosition = -1;
private List<String> lastCompletions;
private int lastShownCompletion;
private List<String> currentSuggestions;
private int currentShownCompletion;
private FocusListener nextFocusListener;

public AutoCompleteListener(AutoCompleter<String> completer) {
Expand All @@ -56,33 +56,29 @@ public void setNextFocusListener(FocusListener listener) {

@Override
public void keyPressed(KeyEvent e) {
if ((textToInsert != null) && (e.getKeyCode() == KeyEvent.VK_ENTER)) {
JTextComponent comp = (JTextComponent) e.getSource();
int keyCode = e.getKeyCode();
JTextComponent textField = (JTextComponent) e.getSource();

// ENTER inserts auto-completion
if ((textToInsert != null) && (keyCode == KeyEvent.VK_ENTER)) {
// replace typed characters by characters from completion
lastBeginning = lastCompletions.get(lastShownCompletion);
currentWord = currentSuggestions.get(currentShownCompletion);

int end = comp.getSelectionEnd();
comp.select(end, end);
int end = textField.getSelectionEnd();
textField.select(end, end);
textToInsert = null;
e.consume();
}
// Cycle through alternative completions when user presses PGUP/PGDN:
else if ((e.getKeyCode() == KeyEvent.VK_PAGE_DOWN) && (textToInsert != null)) {
// Cycle through alternative completions with PGUP/PGDN
else if ((keyCode == KeyEvent.VK_PAGE_DOWN) && (textToInsert != null)) {
cycleSuggestion((JTextComponent) e.getSource(), 1);
e.consume();
} else if ((e.getKeyCode() == KeyEvent.VK_PAGE_UP) && (textToInsert != null)) {
} else if ((keyCode == KeyEvent.VK_PAGE_UP) && (textToInsert != null)) {
cycleSuggestion((JTextComponent) e.getSource(), -1);
e.consume();
}
// else if ((e.getKeyCode() == KeyEvent.VK_BACK_SPACE)) {
// StringBuffer currentword = getCurrentWord((JTextComponent) e.getSource());
// // delete last char to obey semantics of back space
// currentword.deleteCharAt(currentword.length()-1);
// doCompletion(currentword, e);
// }
else if (e.getKeyChar() == KeyEvent.CHAR_UNDEFINED) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
} else if (e.getKeyChar() == KeyEvent.CHAR_UNDEFINED) {
if (keyCode == KeyEvent.VK_SHIFT) {
// TODO: what does this handle here?
// shift is OK, everything else leads to a reset
LOGGER.debug("Special case: shift pressed. No action.");
} else {
Expand All @@ -92,32 +88,32 @@ else if (e.getKeyChar() == KeyEvent.CHAR_UNDEFINED) {
}

private void cycleSuggestion(JTextComponent textField, int increment) {
assert (lastCompletions != null);
assert (!lastCompletions.isEmpty());

lastShownCompletion += increment;
if (lastShownCompletion >= lastCompletions.size()) {
lastShownCompletion = 0;
} else if (lastShownCompletion < 0) {
lastShownCompletion = lastCompletions.size() - 1;
assert (currentSuggestions != null);
assert (!currentSuggestions.isEmpty());

currentShownCompletion += increment;
if (currentShownCompletion >= currentSuggestions.size()) {
currentShownCompletion = 0;
} else if (currentShownCompletion < 0) {
currentShownCompletion = currentSuggestions.size() - 1;
}

// new suggestion
String newSuggestion = lastCompletions.get(lastShownCompletion);
textToInsert = newSuggestion.substring(lastBeginning.length() - 1);
String newSuggestion = currentSuggestions.get(currentShownCompletion);
textToInsert = newSuggestion.substring(currentWord.length() - 1);

// replace suggestion selection with new suggestion
StringBuilder text = new StringBuilder(textField.getText());

int oldSelectionStart = textField.getSelectionStart();
int oldSelectionEnd = textField.getSelectionEnd();

// replace prefix with new prefix
int startPos = textField.getSelectionStart() - lastBeginning.length();
// replace currentWord with prefix of suggestion, e.g. app -> App
int startPos = textField.getSelectionStart() - currentWord.length();
text.delete(startPos, oldSelectionStart);
text.insert(startPos, newSuggestion.subSequence(0, lastBeginning.length()));
text.insert(startPos, newSuggestion.subSequence(0, currentWord.length()));

// replace suffix with new suffix
// replace suffix of suggestion
text.delete(oldSelectionStart, oldSelectionEnd);
text.insert(oldSelectionStart, textToInsert.substring(1));

Expand All @@ -133,24 +129,25 @@ private void cycleSuggestion(JTextComponent textField, int increment) {
* If user cancels autocompletion by a) entering another letter than the completed word (and there is no other auto
* completion) b) space the casing of the letters has to be kept
*
* Global variable "lastBeginning" keeps track of typed letters. We rely on this variable to reconstruct the text
* Global variable "currentWord" keeps track of typed letters. We rely on this variable to reconstruct the text
*
* @param wordSeperatorTyped indicates whether the user has typed a white space character or a
* @param wordSeparatorTyped indicates whether the user has typed a white space character or a
*/
private void setUnmodifiedTypedLetters(JTextComponent comp, boolean lastBeginningContainsTypedCharacter,
boolean wordSeperatorTyped) {
if (lastBeginning == null) {
boolean wordSeparatorTyped) {
if (currentWord == null) {
LOGGER.debug("No last beginning found");
// There was no previous input (if the user typed a word, where no autocompletion is available)
// Thus, there is nothing to replace
return;
}
LOGGER.debug("lastBeginning: >" + lastBeginning + '<');

LOGGER.debug("currentWord: >" + currentWord + '<');
if (comp.getSelectedText() == null) {
// if there is no selection
// the user has typed the complete word, but possibly with a different casing
// we need a replacement
if (wordSeperatorTyped) {
if (wordSeparatorTyped) {
LOGGER.debug("Replacing complete word");
} else {
// if user did not press a white space character (space, ...),
Expand All @@ -165,27 +162,23 @@ private void setUnmodifiedTypedLetters(JTextComponent comp, boolean lastBeginnin

lastCaretPosition = comp.getCaretPosition();

int endIndex = lastCaretPosition - lastBeginning.length();
int endIndex = lastCaretPosition - currentWord.length();
if (lastBeginningContainsTypedCharacter) {
// the current letter is NOT contained in comp.getText(), but in lastBeginning
// thus lastBeginning.length() is one too large
// the current letter is NOT contained in comp.getText(), but in currentWord
// thus currentWord.length() is one too large
endIndex++;
}
String text = comp.getText();
comp.setText(text.substring(0, endIndex).concat(lastBeginning).concat(text.substring(lastCaretPosition)));
comp.setText(text.substring(0, endIndex).concat(currentWord).concat(text.substring(lastCaretPosition)));
if (lastBeginningContainsTypedCharacter) {
// the current letter is NOT contained in comp.getText()
// Thus, cursor position also did not get updated
lastCaretPosition++;
}
comp.setCaretPosition(lastCaretPosition);
lastBeginning = null;
currentWord = null;
}

/**
* Start a new completion attempt (instead of treating a continuation of an existing word or an interrupt of the
* current word)
*/
private void startCompletion(String currentWord, KeyEvent e) {
JTextComponent textField = (JTextComponent) e.getSource();
List<String> suggestions = completer.complete(currentWord);
Expand All @@ -194,9 +187,9 @@ private void startCompletion(String currentWord, KeyEvent e) {
return;
}

lastShownCompletion = 0;
lastCompletions = suggestions;
currentSuggestions = suggestions;
// we use the first word inside the suggestions
currentShownCompletion = 0;
String suggestedWord = suggestions.get(0);

// these two lines obey the user's input
Expand All @@ -215,12 +208,10 @@ private void startCompletion(String currentWord, KeyEvent e) {
lastCaretPosition = textField.getCaretPosition();
char ch = e.getKeyChar();

LOGGER.debug("Appending >" + ch + '<');

if (currentWord.length() <= 1) {
lastBeginning = Character.toString(ch);
this.currentWord = Character.toString(ch);
} else {
lastBeginning = currentWord.substring(0, currentWord.length() - 1).concat(Character.toString(ch));
this.currentWord = currentWord.substring(0, currentWord.length() - 1).concat(Character.toString(ch));
}
}

Expand All @@ -240,44 +231,35 @@ public void keyTyped(KeyEvent e) {
|| (Character.isWhitespace(ch) && completer.isSingleUnitField())) {
JTextComponent comp = (JTextComponent) e.getSource();

// TODO: sometimes doesnt work when we continue words only the last char gets recognized
// don't do auto completion inside words
// TODO: ac not working as expected, e.g. Appstand, Application -> App -> Apps (no completion here)
try {
char pos = comp.getText().charAt(comp.getCaretPosition());
if (!Character.isWhitespace(pos)) {
return;
}
} catch (IndexOutOfBoundsException ex) {

if (!atEndOfWord(comp)) {
return;
}

// The case-insensitive system is a bit tricky here
// If keyword is "TODO" and user types "tO", then this is treated as "continue" as the "O" matches the "O"
// If keyword is "TODO" and user types "To", then this is treated as "discont" as the "o" does NOT match the "O".

// User continues on the word that was suggested.
if ((textToInsert != null) && (textToInsert.length() > 1) && (ch == textToInsert.charAt(1))) {
// User continues on the word that was suggested.
LOGGER.debug("cont");

textToInsert = textToInsert.substring(1);
if (!textToInsert.isEmpty()) {
int cp = comp.getCaretPosition();
//comp.setCaretPosition(cp+1-textToInsert.);
comp.select((cp + 1) - textToInsert.length(), cp);
lastBeginning = lastBeginning + ch;
currentWord = currentWord + ch;

e.consume();
lastCaretPosition = comp.getCaretPosition();

lastCompletions = completer.complete(lastBeginning);
lastShownCompletion = 0;
for (int i = 0; i < lastCompletions.size(); i++) {
String lastCompletion = lastCompletions.get(i);
currentSuggestions = completer.complete(currentWord);
currentShownCompletion = 0;
for (int i = 0; i < currentSuggestions.size(); i++) {
String lastCompletion = currentSuggestions.get(i);
if (lastCompletion.endsWith(textToInsert)) {
lastShownCompletion = i;
currentShownCompletion = i;
break;
}

}
if (textToInsert.length() < 2) {
// User typed the last character of the autocompleted word
Expand All @@ -286,7 +268,7 @@ public void keyTyped(KeyEvent e) {
// "space" indicates that the user does NOT want the autocompletion,
// but the typed word
String text = comp.getText();
comp.setText(text.substring(0, lastCaretPosition - lastBeginning.length()) + lastBeginning
comp.setText(text.substring(0, lastCaretPosition - currentWord.length()) + currentWord
+ text.substring(lastCaretPosition));
// there is no selected text, therefore we are not updating the selection
textToInsert = null;
Expand All @@ -295,25 +277,24 @@ public void keyTyped(KeyEvent e) {
}
}

// User discontinues the word that was suggested.
if ((textToInsert != null) && ((textToInsert.length() <= 1) || (ch != textToInsert.charAt(1)))) {
// User discontinues the word that was suggested.
lastBeginning = lastBeginning + ch;

LOGGER.debug("discont textToInsert: >" + textToInsert + "'<' lastBeginning: >" + lastBeginning + '<');
currentWord = currentWord + ch;

List<String> completed = completer.complete(lastBeginning);
List<String> completed = completer.complete(currentWord);
if ((completed != null) && (!completed.isEmpty())) {
lastShownCompletion = 0;
lastCompletions = completed;
// TODO: insert new completion should be extracted and reused!
currentShownCompletion = 0;
currentSuggestions = completed;
String sno = completed.get(0);
// textToInsert = string used for autocompletion last time
// this string has to be removed
// lastCaretPosition is the position of the caret after textToInsert.
int lastLen = textToInsert.length() - 1;
textToInsert = sno.substring(lastBeginning.length() - 1);
textToInsert = sno.substring(currentWord.length() - 1);
String text = comp.getText();
//we do not use textToInsert as we want to obey the casing of "sno"
comp.setText(text.substring(0, (lastCaretPosition - lastLen - lastBeginning.length()) + 1) + sno
comp.setText(text.substring(0, (lastCaretPosition - lastLen - currentWord.length()) + 1) + sno
+ text.substring(lastCaretPosition));
int startSelect = (lastCaretPosition + 1) - lastLen;
int endSelect = (lastCaretPosition + textToInsert.length()) - lastLen;
Expand All @@ -330,10 +311,9 @@ public void keyTyped(KeyEvent e) {
}
}

// TODO: new auto completion?
comp.replaceSelection("");

StringBuffer currentword = getCurrentWord(comp);

// only "real characters" end up here
assert (!Character.isISOControl(ch));
currentword.append(ch);
Expand Down Expand Up @@ -361,6 +341,19 @@ public void keyTyped(KeyEvent e) {
resetAutoCompletion();
}

private boolean atEndOfWord(JTextComponent textField) {
int nextCharPosition = textField.getCaretPosition();

// position not at the end of input
if(nextCharPosition < textField.getText().length()) {
char nextChar = textField.getText().charAt(nextCharPosition);
if (!Character.isWhitespace(nextChar)) {
return false;
}
}
return true;
}

private StringBuffer getCurrentWord(JTextComponent textField) {
final int caretPosition = textField.getCaretPosition();

Expand Down Expand Up @@ -424,7 +417,7 @@ public void focusLost(FocusEvent event) {
private void resetAutoCompletion() {
LOGGER.debug("Resetting autocompletion");
textToInsert = null;
lastBeginning = null;
currentWord = null;
}

public void clearCurrentSuggestion(JTextComponent comp) {
Expand All @@ -433,8 +426,8 @@ public void clearCurrentSuggestion(JTextComponent comp) {
String text = comp.getText();
comp.setText(text.substring(0, selStart) + text.substring(comp.getSelectionEnd()));
comp.setCaretPosition(selStart);
lastCompletions = null;
lastShownCompletion = 0;
currentSuggestions = null;
currentShownCompletion = 0;
lastCaretPosition = -1;
textToInsert = null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,14 @@ public List<String> complete(String toComplete) {
return Collections.emptyList();
}

String lowerCase = toComplete.toLowerCase();
//String lowerCase = toComplete.toLowerCase();

// TODO: does this decision make sense?
// TODO: does this decision make sense? strange behavior possible, as your typed word may change casing
// e.g. app -> Application
// user typed in lower case word -> we do an case-insensitive search
if (lowerCase.equals(toComplete)) {
// also why not do lowercase search all the time? The decision that we only search lowercase when the user types lowercase is BS?!
// we should make this configurable! default is case sensitive for all linux bashes
/*if (lowerCase.equals(toComplete)) {
String ender = incrementLastCharacter(lowerCase);
SortedSet<String> subset = indexCaseInsensitive.subSet(lowerCase, ender);
Expand All @@ -68,13 +71,11 @@ public List<String> complete(String toComplete) {
result.addAll(possibleStringsForLowercaseSearch.get(s));
}
return result;
}
// user typed in a mix of upper case and lower case, we assume user wants to have exact search
else {
String ender = incrementLastCharacter(toComplete);
SortedSet<String> subset = indexCaseSensitive.subSet(toComplete, ender);
return new ArrayList<>(subset);
}
}*/

String nextWord = incrementLastCharacter(toComplete);
SortedSet<String> matchingWords = indexCaseSensitive.subSet(toComplete, nextWord);
return new ArrayList<>(matchingWords);
}

/**
Expand Down

0 comments on commit a4c5c0f

Please sign in to comment.