I have seen many questions on autocompeletion for java text components like JTextField JTextArea JTextEditorPane etc.

There are not many options either:

1) 3rd party library (like SwingX)
2) DIY (i.e using DocumentListener, JWindow with JLabel etc and a few requestFocusInWindow calls)

I chose number 2 and put the code up here for others to have a working foundation that can be improved to your needs.

Basically you would just make sure the AutoSuggestor class is within package hierarchy than you would do something like:

        //create JTextComponent that we want to make as AutoSuggestor
        //JTextField f = new JTextField(10);
        JTextArea f = new JTextArea(10, 10);
        //JEditorPane f = new JEditorPane();

        //create words for dictionary could also use null as parameter for AutoSuggestor(..,..,null,..,..,..,..) and than call AutoSuggestor#setDictionary after AutoSuggestr insatnce has been created
        ArrayList<String> words = new ArrayList<>();
        words.add("hello");
        words.add("heritage");
        words.add("happiness");
        words.add("goodbye");
        words.add("cruel");
        words.add("car");
        words.add("war");
        words.add("will");
        words.add("world");
        words.add("wall");

        AutoSuggestor autoSuggestor = new AutoSuggestor(f, frame, words, Color.WHITE.brighter(), Color.BLUE, Color.RED, 0.75f);

A suggestion cna be clicked with the mouse or alternatively the down key can be use to traverse suggestions and the textcomponent

JTextField pop up window will be shown under the component:

8b41e7d1c4627b8d4248d53df62907e1

while JTextArea and any other JTextComponents will have it visible under the Carets current position:
b50012c98765c57a4ed1ac488e06db35

Hope it helps others.

Edited 3 Years Ago by DavidKroukamp

import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JWindow;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.border.LineBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;

/**
 * @author David
 */
public class Test {

    public Test() {

        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        //JTextField f = new JTextField(10);
        JTextArea f = new JTextArea(10, 10);
        //JEditorPane f = new JEditorPane();

        //create words for dictionary could also use null as parameter for AutoSuggestor(..,..,null,..,..,..,..) and than call AutoSuggestor#setDictionary after AutoSuggestr insatnce has been created
        ArrayList<String> words = new ArrayList<>();
        words.add("hello");
        words.add("heritage");
        words.add("happiness");
        words.add("goodbye");
        words.add("cruel");
        words.add("car");
        words.add("war");
        words.add("will");
        words.add("world");
        words.add("wall");

        AutoSuggestor autoSuggestor = new AutoSuggestor(f, frame, words, Color.WHITE.brighter(), Color.BLUE, Color.RED, 0.75f) {
            @Override
            boolean wordTyped(String typedWord) {
                System.out.println(typedWord);
                return super.wordTyped(typedWord);//checks for a match in dictionary and returns true or false if found or not
            }
        };

        JPanel p = new JPanel();

        p.add(f);

        frame.add(p);

        frame.pack();
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new Test();
            }
        });
    }
}

class AutoSuggestor {

    private final JTextComponent textComp;
    private final Window container;
    private JPanel suggestionsPanel;
    private JWindow autoSuggestionPopUpWindow;
    private String typedWord;
    private final ArrayList<String> dictionary = new ArrayList<>();
    private int currentIndexOfSpace, tW, tH;
    private DocumentListener documentListener = new DocumentListener() {
        @Override
        public void insertUpdate(DocumentEvent de) {
            checkForAndShowSuggestions();
        }

        @Override
        public void removeUpdate(DocumentEvent de) {
            checkForAndShowSuggestions();
        }

        @Override
        public void changedUpdate(DocumentEvent de) {
            checkForAndShowSuggestions();
        }
    };
    private final Color suggestionsTextColor;
    private final Color suggestionFocusedColor;

    public AutoSuggestor(JTextComponent textComp, Window mainWindow, ArrayList<String> words, Color popUpBackground, Color textColor, Color suggestionFocusedColor, float opacity) {
        this.textComp = textComp;
        this.suggestionsTextColor = textColor;
        this.container = mainWindow;
        this.suggestionFocusedColor = suggestionFocusedColor;
        this.textComp.getDocument().addDocumentListener(documentListener);

        setDictionary(words);

        typedWord = "";
        currentIndexOfSpace = 0;
        tW = 0;
        tH = 0;

        autoSuggestionPopUpWindow = new JWindow(mainWindow);
        autoSuggestionPopUpWindow.setOpacity(opacity);

        suggestionsPanel = new JPanel();
        suggestionsPanel.setLayout(new GridLayout(0, 1));
        suggestionsPanel.setBackground(popUpBackground);

        addKeyBindingToRequestFocusInPopUpWindow();
    }

    private void addKeyBindingToRequestFocusInPopUpWindow() {
        textComp.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "Down released");
        textComp.getActionMap().put("Down released", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ae) {//focuses the first label on popwindow
                for (int i = 0; i < suggestionsPanel.getComponentCount(); i++) {
                    if (suggestionsPanel.getComponent(i) instanceof SuggestionLabel) {
                        ((SuggestionLabel) suggestionsPanel.getComponent(i)).setFocused(true);
                        autoSuggestionPopUpWindow.toFront();
                        autoSuggestionPopUpWindow.requestFocusInWindow();
                        suggestionsPanel.requestFocusInWindow();
                        suggestionsPanel.getComponent(i).requestFocusInWindow();
                        break;
                    }
                }
            }
        });
        suggestionsPanel.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "Down released");
        suggestionsPanel.getActionMap().put("Down released", new AbstractAction() {
            int lastFocusableIndex = 0;

            @Override
            public void actionPerformed(ActionEvent ae) {//allows scrolling of labels in pop window (I know very hacky for now :))

                ArrayList<SuggestionLabel> sls = getAddedSuggestionLabels();
                int max = sls.size();

                if (max > 1) {//more than 1 suggestion
                    for (int i = 0; i < max; i++) {
                        SuggestionLabel sl = sls.get(i);
                        if (sl.isFocused()) {
                            if (lastFocusableIndex == max - 1) {
                                lastFocusableIndex = 0;
                                sl.setFocused(false);
                                autoSuggestionPopUpWindow.setVisible(false);
                                setFocusToTextField();
                                checkForAndShowSuggestions();//fire method as if document listener change occured and fired it

                            } else {
                                sl.setFocused(false);
                                lastFocusableIndex = i;
                            }
                        } else if (lastFocusableIndex <= i) {
                            if (i < max) {
                                sl.setFocused(true);
                                autoSuggestionPopUpWindow.toFront();
                                autoSuggestionPopUpWindow.requestFocusInWindow();
                                suggestionsPanel.requestFocusInWindow();
                                suggestionsPanel.getComponent(i).requestFocusInWindow();
                                lastFocusableIndex = i;
                                break;
                            }
                        }
                    }
                } else {//only a single suggestion was given
                    autoSuggestionPopUpWindow.setVisible(false);
                    setFocusToTextField();
                    checkForAndShowSuggestions();//fire method as if document listener change occured and fired it
                }
            }
        });
    }

    private void setFocusToTextField() {
        container.toFront();
        container.requestFocusInWindow();
        textComp.requestFocusInWindow();
    }

    public ArrayList<SuggestionLabel> getAddedSuggestionLabels() {
        ArrayList<SuggestionLabel> sls = new ArrayList<>();
        for (int i = 0; i < suggestionsPanel.getComponentCount(); i++) {
            if (suggestionsPanel.getComponent(i) instanceof SuggestionLabel) {
                SuggestionLabel sl = (SuggestionLabel) suggestionsPanel.getComponent(i);
                sls.add(sl);
            }
        }
        return sls;
    }

    private void checkForAndShowSuggestions() {
        typedWord = getCurrentlyTypedWord();

        suggestionsPanel.removeAll();//remove previos words/jlabels that were added

        //used to calcualte size of JWindow as new Jlabels are added
        tW = 0;
        tH = 0;

        boolean added = wordTyped(typedWord);

        if (!added) {
            if (autoSuggestionPopUpWindow.isVisible()) {
                autoSuggestionPopUpWindow.setVisible(false);
            }
        } else {
            showPopUpWindow();
            setFocusToTextField();
        }
    }

    protected void addWordToSuggestions(String word) {
        SuggestionLabel suggestionLabel = new SuggestionLabel(word, suggestionFocusedColor, suggestionsTextColor, this);

        calculatePopUpWindowSize(suggestionLabel);

        suggestionsPanel.add(suggestionLabel);
    }

    public String getCurrentlyTypedWord() {//get newest word after last white spaceif any or the first word if no white spaces
        String text = textComp.getText();
        String wordBeingTyped = "";
        text = text.replaceAll("(\\r|\\n)", " ");//replace end of line characters
        if (text.contains(" ")) {
            int tmp = text.lastIndexOf(" ");
            if (tmp >= currentIndexOfSpace) {
                currentIndexOfSpace = tmp;
                wordBeingTyped = text.substring(text.lastIndexOf(" "));
            }
        } else {
            wordBeingTyped = text;
        }
        return wordBeingTyped.trim();
    }

    private void calculatePopUpWindowSize(JLabel label) {
        //so we can size the JWindow correctly
        if (tW < label.getPreferredSize().width) {
            tW = label.getPreferredSize().width;
        }
        tH += label.getPreferredSize().height;
    }

    private void showPopUpWindow() {
        autoSuggestionPopUpWindow.getContentPane().add(suggestionsPanel);
        autoSuggestionPopUpWindow.setMinimumSize(new Dimension(textComp.getWidth(), 30));
        autoSuggestionPopUpWindow.setSize(tW, tH);
        autoSuggestionPopUpWindow.setVisible(true);

        int windowX = 0;
        int windowY = 0;

        if (textComp instanceof JTextField) {//calculate x and y for JWindow at bottom of JTextField
            windowX = container.getX() + textComp.getX() + 5;
            if (suggestionsPanel.getHeight() > autoSuggestionPopUpWindow.getMinimumSize().height) {
                windowY = container.getY() + textComp.getY() + textComp.getHeight() + autoSuggestionPopUpWindow.getMinimumSize().height;
            } else {
                windowY = container.getY() + textComp.getY() + textComp.getHeight() + autoSuggestionPopUpWindow.getHeight();
            }
        } else {//calculate x and y for JWindow on any JTextComponent using the carets position
            Rectangle rect = null;
            try {
                rect = textComp.getUI().modelToView(textComp, textComp.getCaret().getDot());//get carets position
            } catch (BadLocationException ex) {
                ex.printStackTrace();
            }

            windowX = (int) (rect.getX() + 15);
            windowY = (int) (rect.getY() + (rect.getHeight() * 3));
        }

        //show the pop up
        autoSuggestionPopUpWindow.setLocation(windowX, windowY);
        autoSuggestionPopUpWindow.setMinimumSize(new Dimension(textComp.getWidth(), 30));
        autoSuggestionPopUpWindow.revalidate();
        autoSuggestionPopUpWindow.repaint();

    }

    public void setDictionary(ArrayList<String> words) {
        dictionary.clear();
        if (words == null) {
            return;//so we can call constructor with null value for dictionary without exception thrown
        }
        for (String word : words) {
            dictionary.add(word);
        }
    }

    public JWindow getAutoSuggestionPopUpWindow() {
        return autoSuggestionPopUpWindow;
    }

    public Window getContainer() {
        return container;
    }

    public JTextComponent getTextField() {
        return textComp;
    }

    public void addToDictionary(String word) {
        dictionary.add(word);
    }

    boolean wordTyped(String typedWord) {

        if (typedWord.isEmpty()) {
            return false;
        }
        //System.out.println("Typed word: " + typedWord);

        boolean suggestionAdded = false;

        for (String word : dictionary) {//get words in the dictionary which we added
            boolean fullymatches = true;
            for (int i = 0; i < typedWord.length(); i++) {//each string in the word
                if (!typedWord.toLowerCase().startsWith(String.valueOf(word.toLowerCase().charAt(i)), i)) {//check for match
                    fullymatches = false;
                    break;
                }
            }
            if (fullymatches) {
                addWordToSuggestions(word);
                suggestionAdded = true;
            }
        }
        return suggestionAdded;
    }

    class SuggestionLabel extends JLabel {

        private boolean focused = false;
        private final JWindow autoSuggestionsPopUpWindow;
        private final JTextComponent textComponent;
        private final AutoSuggestor autoSuggestor;
        private Color suggestionsTextColor, suggestionBorderColor;

        public SuggestionLabel(String string, final Color borderColor, Color suggestionsTextColor, AutoSuggestor autoSuggestor) {
            super(string);

            this.suggestionsTextColor = suggestionsTextColor;
            this.autoSuggestor = autoSuggestor;
            this.textComponent = autoSuggestor.getTextField();
            this.suggestionBorderColor = borderColor;
            this.autoSuggestionsPopUpWindow = autoSuggestor.getAutoSuggestionPopUpWindow();

            initComponent();
        }

        private void initComponent() {
            setFocusable(true);
            setForeground(suggestionsTextColor);

            addMouseListener(new MouseAdapter() {//so we can click on suggestion with mouse too
                @Override
                public void mouseClicked(MouseEvent me) {
                    super.mouseClicked(me);

                    replaceWithSuggestedText();

                    autoSuggestionsPopUpWindow.setVisible(false);
                }
            });

            getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, true), "Enter released");
            getActionMap().put("Enter released", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    replaceWithSuggestedText();
                    autoSuggestionsPopUpWindow.setVisible(false);
                }
            });
        }

        public void setFocused(boolean focused) {
            if (focused) {
                setBorder(new LineBorder(suggestionBorderColor));
            } else {
                setBorder(null);
            }
            repaint();
            this.focused = focused;
        }

        public boolean isFocused() {
            return focused;
        }

        private void replaceWithSuggestedText() {
            String suggestedWord = getText();
            String text = textComponent.getText();
            String typedWord = autoSuggestor.getCurrentlyTypedWord();
            String t = text.substring(0, text.lastIndexOf(typedWord));
            String tmp = t + text.substring(text.lastIndexOf(typedWord)).replace(typedWord, suggestedWord);
            textComponent.setText(tmp + " ");
        }
    }
}

@David, probably not an answer

  • why reinvent the wheel (as you know very well I'm always against...), use standard code made by Sun or SwingX (has issue with Carret & Highlighter)

  • maybe not to use KeyBindings or KeyListener, job for AWTEventListener (key & mouse events)

  • I miss there Carret and something about Bias

  • notice JTextField (JTextComponents, no idea why, never tried to solve that somehow) aren't editable in JWindow by default, required undecorated JDialog

  • interesting idea about JLabel +1 then ...

_____________________________________________________________________________________________________________________________________________________________

  • I'be use JTable, better impementations of Collator, Comparator in APi, XxxRenderer is easy to use for this job

  • with RowFilter (required override two code lines for convert row and column view to model)

  • with one column, two three, depends of (by default all autocompleted bysed on larger arrays to reacting on 2-3 chars typed, to avoiding huge computing for mathces from Comparator)

  • without JTableHeader

Edited 3 Years Ago by mKorbel

I think autocompletion for JTextComponents is a great idea, but I'm slightly disappointed by that implementation. With this one, you just press tab and it instantly finishes your word for you. It doesn't display a list, but you can press tab repeatedly to get new guesses or type more letters. This is closer to how I think an autocompleter should be.

/** Given a prefix, a Completer provides a Completion that can be used to get
 *  alternate completions for the prefix. */
public interface Completer {
    public Completion complete(String prefix);
}

/** An unmodifiable value that represents one possible guess for the rest of a word based on a prefix.
 *  It should be safe to use as a key in a Map for potential pop-up window implementations. */
public interface Completion {
    public String completionText();
    public Completion nextCompletion();
}

import java.awt.event.ActionEvent;
import java.util.Arrays;
import javax.swing.*;
import javax.swing.text.*;

/** DocumentFilter that makes guesses about what is being typed and preemptively inserts its guesses
 * into the Document. It temporarily remembers the location of its guess and the location of the
 * prefix that it is using to guess so it can update its guess as more of the prefix is typed. Each
 * AutoTyper is connected to the JTextComponent that it types into. The Document of the
 * JTextComponent must be an AbstractDocument and it must not be replaced while the AutoTyper is
 * active. The caret position is moved every time AutoTyper updates its guess so that it is always
 * at the beginning of the guess where the user is expected to be typing. If the Document is edited
 * anywhere other than the beginning of the guess, AutoTyper assumes that the guess is correct and
 * deactivates itself. If something is inserted at the start of the guess that is neither part of
 * any word nor a letter or digit, then AutoTyper assumes that the user has accepted the guess and
 * moves the caret to the end of the guess before inserting the new text. This allows the user to
 * accept the guess by pressing space or typing punctuation. */
public final class AutoTyper extends DocumentFilter {
    public final Action completeAction = new AbstractAction("Complete") {
        @Override
        public void actionPerformed(ActionEvent e) {
            if(active) advanceToNextCompletion();
            else activate();
        }
    };
    public final Action selectAction = new AbstractAction("Select") {
        @Override
        public void actionPerformed(ActionEvent e) {
            if(active) select();
        }
    };
    private final JTextComponent component;
    private final Completer completer;
    private AbstractDocument document;
    /** When true, wordStart, insertStart, and insertLength must have meaningful values */
    private boolean active;
    private int wordStart; // The start of the word we are guessing, undefined if !active
    private int insertStart; // The start of guessed text, undefined if !active
    private int insertLength; // The length of guessed text, undefined if !active
    private Completion currentCompletion;
    private AttributeSet attributeSet; // The font to use for guesses
    public AutoTyper(JTextComponent component, Completer completer) {
        if(completer == null) throw new NullPointerException();
        this.component = component;
        this.completer = completer;
    }
    /** The style of text typed automatically, probably the same style that the user is typing in. */
    public void setAttributes(AttributeSet attr) {
        this.attributeSet = attr;
    }
    /** Accept the current guess and move the caret to the end of the guess. */
    public void select() {
        if(!active) throw new IllegalStateException();
        component.setCaretPosition(insertStart + insertLength);
        deactivate();
    }
    /** Use the caret position to find the start of the current word, then insert a guess for the
     * rest of the word. */
    public void activate() {
        try {
            document = (AbstractDocument)component.getDocument();
            insertStart = component.getCaretPosition();
            wordStart = findWordStart(document, insertStart);
            insertLength = 0;
            innerActivate();
            currentCompletion = completer.complete(getPrefix());
            update(null);
        } catch (BadLocationException e) {
            throw new RuntimeException(e);
        }
    }
    /** Use the given start of the word and the current caret position to determine the current
     * word-prefix being typed, then insert a guess for the rest of the word. */
    public void activate(int wordStart) throws BadLocationException {
        document = (AbstractDocument)component.getDocument();
        if(wordStart < 0 || wordStart > document.getLength()) throw new BadLocationException("Bad wordStart", wordStart);
        final int caret = component.getCaretPosition();
        if(caret < wordStart) throw new IllegalArgumentException("wordStart:" + wordStart + " caret:" + caret);
        this.wordStart = wordStart;
        this.insertStart = caret;
        this.insertLength = 0;
        innerActivate();
        currentCompletion = completer.complete(getPrefix());
        update(null);
    }
    private void innerActivate() {
        active = true; // The only place active is set to true
        document.setDocumentFilter(this);
    }
    /** Deactivate AutoTyper without removing the current guess or moving the caret. */
    public void deactivate() {
        document.setDocumentFilter(null);
        active = false; // The only place active is set to false
    }
    public boolean isActive() {
        return active;
    }
    public Completer getCompleter() {
        return completer;
    }
    public String getPrefix() {
        if(!active) throw new IllegalStateException();
        try {
            return document.getText(wordStart, insertStart - wordStart);
        } catch (BadLocationException e) {
            throw new RuntimeException(e);
        }
    }
    /** Replace the current guess with the given guess. */
    public void setCompletion(Completion completion) {
        try {
            currentCompletion = completion;
            update(null);
        } catch (BadLocationException e) {
            throw new RuntimeException(e);
        }
    }
    public Completion getCompletion() {
        return currentCompletion;
    }
    /** Replace the current guess with another guess. */
    public void advanceToNextCompletion() {
        Completion c = currentCompletion.nextCompletion();
        if(c == null && active) c = completer.complete(getPrefix());
        setCompletion(c);
    }
    @Override
    public void remove(FilterBypass fb, int offset, int length) throws BadLocationException {
        if(length == 0) return;
        fb.remove(offset, length);
        if(!active) return;
        else if(offset + length == insertStart) {
            insertStart = offset;
            if(wordStart > offset) {
                wordStart = offset;
                currentCompletion = null;
            }
            else currentCompletion = completer.complete(getPrefix());
            update(fb);
        } else deactivate();
    }
    @Override
    public void insertString(FilterBypass fb, int offset, String text, AttributeSet attr) throws BadLocationException {
        if(!active) fb.insertString(offset, text, attr);
        else if(offset == insertStart) {
            if(isSelectionTrigger(text)) {
                select();
                fb.insertString(component.getCaretPosition(), text, attr);
                return;
            }
            fb.insertString(offset, text, attr);
            insertStart = offset + text.length();
            currentCompletion = completer.complete(getPrefix());
            update(fb);
        } else {
            fb.insertString(offset, text, attr);
            deactivate();
        }
    }
    @Override
    public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attr)
        throws BadLocationException {
        if(!active) fb.replace(offset, length, text, attr);
        else if(offset + length == insertStart) {
            remove(fb, offset, length);
            insertString(fb, offset, text, attr);
        } else {
            fb.replace(offset, length, text, attr);
            deactivate();
        }
    }
    /** Does typing this text mean the user is accepting the current guess? */
    private boolean isSelectionTrigger(String text) {
        if(text.length() != 1) return false;
        final char c = text.charAt(0);
        if(Character.isLetterOrDigit(c)) return false;
        final String prefix = getPrefix();
        return completer.complete(prefix + c) == null;
    }
    /** Remove the text in the document that is supposed to be the current guess and replace it with
     * the actual current guess. Also update insertLength. If there is no current guess, deactivate
     * after removing the text.
     * 
     * @param fb FilterBypass to use when editing the document, or null to edit the document
     *            directly. */
    private void update(FilterBypass fb) throws BadLocationException {
        if(!active) return;
        // If we are editing the document directly, deactivate ourselves during the edit
        if(fb == null) deactivate();
        if(insertLength > 0) {
            if(fb == null) document.remove(insertStart, insertLength);
            else fb.remove(insertStart, insertLength);
        }
        if(currentCompletion == null) {
            deactivate();
            insertLength = 0;
            return;
        }
        insertLength = currentCompletion.completionText().length();
        if(fb == null) document.insertString(insertStart, currentCompletion.completionText(), attributeSet);
        else fb.insertString(insertStart, currentCompletion.completionText(), attributeSet);
        // If we are editing the document directly, re-activate
        if(fb == null) innerActivate();
        // The insertion moved the caret forward, so put it back where it should be
        component.setCaretPosition(insertStart);
    }
    private static int findWordStart(Document doc, int insertPoint) throws BadLocationException {
        Segment text = new Segment();
        text.setPartialReturn(true);
        int pos = 0;
        int lastWordBreak = -1;
        while (pos < insertPoint) {
            doc.getText(pos, insertPoint - pos, text);
            for (int i = 0; i < text.count; i++) {
                if(!Character.isLetterOrDigit(text.array[i])) lastWordBreak = pos + i;
            }
            pos += text.count;
        }
        return lastWordBreak + 1;
    }
    // Demonstration
    private static final String[] words = {"about","after","again","against","alone","along","another","around",
        "because","before","below","between","Hello","heritage","happiness","goodbye","cruel","car","war","will",
        "world","wall"};
    public static void main(String... args) {
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                final JFrame frame = new JFrame("AutoTyper");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                final JTextArea area = new JTextArea();
                final Completer completer = new TrieCompleter(Arrays.asList(words));
                final AutoTyper typer = new AutoTyper(area, completer);
                final Keymap keymap = JTextComponent.addKeymap(null, area.getKeymap());
                area.setKeymap(keymap);
                keymap.addActionForKeyStroke(KeyStroke.getKeyStroke("TAB"), typer.completeAction);
                frame.add(area);
                frame.setSize(300, 200);
                frame.setVisible(true);
            }
        });
    }
}

import java.util.*;

/** Case-sensitive completer using a trie.
 *  A non-case-sensitive completer could be built around a TrieCompleter. */
public final class TrieCompleter implements Completer {
    /** Stack that we can pop and push nondestructively */
    private final class NodeStack {
        final Node node;
        final NodeStack rest;
        public NodeStack(Node node, NodeStack rest) {
            this.node = node;
            this.rest = rest;
        }
        private int size() {
            if(rest == null) return 1;
            else return rest.size() + 1;
        }
    }
    /** Immutable completion that represents its position as a stack of nodes */
    private final class Comp implements Completion {
        private final NodeStack stack;
        private final String text; // redundant with stack
        public Comp(NodeStack stack, String text) {
            this.stack = stack;
            this.text = text;
        }
        @Override
        public int hashCode() {
            return stack.node.hashCode() * 31 + text.length();
        }
        @Override
        public boolean equals(Object o) {
            if(!(o instanceof Comp)) return false;
            final Comp c = (Comp)o;
            return c.stack.node == stack.node && c.text.length() == text.length();
        }
        @Override
        public String completionText() {
            return text;
        }
        @Override
        public Completion nextCompletion() {
            // Maintain string and stack at the same length
            StringBuilder sb = new StringBuilder(text);
            NodeStack stack = this.stack;
            // Look for longer words with this word as a prefix
            if(stack.node.firstChild != null) {
                Node node = stack.node.firstChild;
                sb.append(node.value);
                stack = new NodeStack(node, stack);
                while (!node.isWord) {
                    node = node.firstChild;
                    sb.append(node.value);
                    stack = new NodeStack(node, stack);
                }
                return new Comp(stack, sb.toString());
            }
            // If there are no longer words, advance one letter to the next letter
            while (stack.node.next == null) {
                // Remove each letter from the end of the word until we find a letter that has a
                // next letter
                stack = stack.rest;
                if(stack == null) return null;
                sb.setLength(sb.length() - 1);
            }
            Node node = stack.node.next;
            // This letter has a next letter, so remove it and replace it with the next letter
            sb.setLength(sb.length() - 1);
            stack = stack.rest;
            sb.append(node.value);
            stack = new NodeStack(node, stack);
            // Add letters to the end of the word until we have a real word
            while (!node.isWord) {
                node = node.firstChild;
                sb.append(node.value);
                stack = new NodeStack(node, stack);
            }
            return new Comp(stack, sb.toString());
        }
    }
    private final class Node {
        final char value;
        final Map<Character,Node> children = new HashMap<Character,Node>();
        public Node(char value) {
            this.value = value;
        }
        /** This node represents the last letter in a word */
        boolean isWord;
        /** Start here when iterating this node's children. This should not be null if isWord is
         * false. */
        Node firstChild;
        /** The letter that follows this letter when iterating. next should be a node with the same
         * parent as this node. */
        Node next;
        public Node get(char value) {
            return children.get(value);
        }
        public void addChild(char value, Node child) {
            child.next = firstChild;
            firstChild = child;
            children.put(value, child);
        }
    }
    private final Node firstNode;
    /** Construct a trie to represent the given words. No words can be added or removed once the trie
     * is constructed. If the words are given in alphabetically order, the trie will be in
     * alphabetical order. */
    public TrieCompleter(Collection<String> words) {
        List<String> reversedWords = new ArrayList<String>(words);
        Collections.reverse(reversedWords); // The trie is constructed backward
        firstNode = new Node('\0');
        for (String word : reversedWords) {
            Node current = firstNode;
            for (int i = 0; i < word.length(); i++) {
                char c = word.charAt(i);
                Node n = current.get(c);
                if(n == null) {
                    n = new Node(c);
                    current.addChild(c, n);
                }
                current = n;
            }
            current.isWord = true;
        }
    }
    @Override
    public Completion complete(String prefix) {
        // Find the node that represents the prefix
        Node node = firstNode;
        for (int i = 0; i < prefix.length(); i++) {
            node = node.get(prefix.charAt(i));
            if(node == null) return null;
        }
        // Construct the rest of the word
        StringBuilder sb = new StringBuilder();
        NodeStack stack = null;
        node = node.firstChild;
        // If the prefix node has no children, then there are no completions
        if(node == null) return null;
        sb.append(node.value);
        stack = new NodeStack(node, stack);
        while (!node.isWord) {
            node = node.firstChild;
            sb.append(node.value);
            stack = new NodeStack(node, stack);
        }
        return new Comp(stack, sb.toString());
    }
}

Edited 3 Years Ago by bguild

The article starter has earned a lot of community kudos, and such articles offer a bounty for quality replies.