Moving the caret after a completion

Questions on using RSyntaxTextArea should go here.

Moderator: robert

Moving the caret after a completion

Postby preditcon » Fri Apr 13, 2012 7:27 am

So I have my XML auto completion up and running but I'm trying to improve it by making it's use more intuitive.

One thing that my users currently have to do after each start-tag completion is to add end-tags manually by typing "<\" after which a proper end tag is added. This is problematic for two reasons: first, the user has to do stuff which the editor should do for her, and second, the caret is always moved to the end of each completion. This then requires a manual move of the caret to the in-between-tags space, so that a value or more children may be added to an XML element (the most common editing command when writing XML).

This is incredibly annoying. So I thought: "Okay. Let's simply provide a completion with both start and end tags (when possible) and then move the caret between them.", but this seems to be easier said than done. Is there a way to somehow move the caret after a successful completion has been done - and only for certain completions?

Details of what I wish to do (where | is the caret):
Code: Select all
1. Editor state: "<con|"
2. [CTRL+Space]
3. [select <ns1:config xmlns:ns1=''"></ns1:config>]
   Editor state: "<ns1:config xmlns:ns1=''>|</ns1:config>"

Also is there a way for "implicit" auto-completions to be provided? For example if the editor is in the final state which I described above and Enter is pressed at that caret position and implicit auto-complete would be issued which would add a newline and a single tab between the tags (instead of a single newline). I could probably do that by listening for key events myself though.
Posts: 27
Joined: Wed Jan 25, 2012 10:09 am

Re: Moving the caret after a completion

Postby robert » Fri Apr 13, 2012 1:13 pm

I'll admit that the requirement for typing "</" to insert the close tag was a personal preference; I don't really like closing tags auto-inserted when I type the closing ">" of the opening tag. If this were an option (perhaps choose between the way things are now, auto-inserting closing tags, and no closing tag assistance at all), would this alleviate most of your problems?

As for completions where the caret is placed somewhere in the middle of the inserted text, that would definitely be a good addition to the library. The closest thing today is parameterized completions, which doesn't fit your needs since it assumes it's for C-style method parameters (a list of stuff between single-char delimiters, such as parens). No answer for this just yet.

There is no implicit auto-completion support either, but for the specific example you asked about, I believe you could override your TokenMaker's getInsertBreak() method to return an action that checked whether the token at offset (caret-1) was a start tag's closing bracket and the token at offset (caret) was a closing tag's open bracket (if that makes sense). In that case, do your special formatting, otherwise just do a standard replaceSelection("\n"). In fact, I could see this as a feature request for the built-in XML and MXML TokenMakers.

This library has made me appreciate the structure of EditorKits in the Swing text package. Things like that custom newline action would be in a much more logical place there, but have gotten a little messy in RSTA due to my desire to separate simple syntax highlighting/editing from code completion, syntax checking, etc.
User avatar
Posts: 760
Joined: Sat May 10, 2008 5:16 pm

Re: Moving the caret after a completion

Postby preditcon » Fri Apr 13, 2012 2:15 pm

Actually I rather like the end-tag insertion on "</" entered (wouldn't mind if the same happened for ">" too though). This can stay as it is as far as I'm concerned.

The main problem is the caret position after such or any other type completion is done. My current workaround is kind of shaky. I add a DocumentListener to RSTA's document and check whether inserted text contains "></" and then set up a SwingUtilities.invokeLater Runnable whoose task moves the caret to a proper position. The latter has to be done because the insertUpdate(DocumentEvent e) seems to get fired before the actual change to the GUI is made and moving the caret too early results in it being catapulted around out of control. This is bound to fail sooner or later since I rely on timing and stuff I shouldn't be relying on (I think). Worked so far though.

I'll try to implement "implicit" auto-completion the way you suggested next week.

I have successfuly solved the implicit auto-completion problem by following the suggestion made by robert above. The only notable problem I had faced is that you can't just extend AbstractAction to create the action (due to reflection). Instead I used org.fife.ui.rtextarea.RecordableTextAction which provides access to an RSTA component and ripped off some of RSTA's existing code. Below is the code if anyone else is interested.
Code: Select all
     * This TokenMaker is to be used by RSTA through it's API.
    public static class CustomXMLTokenMaker extends XMLTokenMaker {

        private FormattedLineBreakAction lbAction = new FormattedLineBreakAction("lineBreakActionWithFormatting");

        public Action getInsertBreakAction() {return lbAction;}

     * This class is based on RSyntaxTextAreaEditorKit.InsertBreakAction and
     * uses parts of it's code. Simply extending it caused problems.
    private static class FormattedLineBreakAction extends RecordableTextAction {

        public FormattedLineBreakAction(String name) {

        public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {
            if (!textArea.isEditable() || !textArea.isEnabled()) {

            RSyntaxTextArea sta = (RSyntaxTextArea) textArea;
            boolean noSelection = sta.getSelectionStart() == sta.getSelectionEnd();

            handleInsertBreak(sta, noSelection);

        private void handleInsertBreak(RSyntaxTextArea textArea,
                boolean noSelection) {
            if (noSelection && textArea.isAutoIndentEnabled()) {
            } else {

        private static int atEndOfLine(int pos, String s, int sLen) {
            for (int i = pos; i < sLen; i++) {
                if (!RSyntaxUtilities.isWhitespace(s.charAt(i))) {
                    return i;
            return -1;

        private void insertNewlineWithAutoIndent(RSyntaxTextArea sta) {
            try {
                int caretPos = sta.getCaretPosition();
                Document doc = sta.getDocument();
                Element map = doc.getDefaultRootElement();
                int lineNum = map.getElementIndex(caretPos);
                Element line = map.getElement(lineNum);
                int start = line.getStartOffset();
                int end = line.getEndOffset() - 1;
                int len = end - start;
                String s = doc.getText(start, len);

                String leadingWS = RSyntaxUtilities.getLeadingWhitespace(s);
                StringBuilder sb = new StringBuilder("\n");

                int nonWhitespacePos = atEndOfLine(caretPos - start, s, len);
                if (nonWhitespacePos == -1) {
                    if (leadingWS.length() == len
                            && sta.isClearWhitespaceLinesEnabled()) {
                else {
                    // determine if we should insert special formatting
                    boolean canSpecialIndent = false;
                    int lineOfOffset = sta.getLineOfOffset(caretPos);
                    Token tokenList = sta.getTokenListForLine(lineOfOffset);
                    Token token = RSyntaxUtilities.getTokenAtOffset(
                            tokenList, caretPos - 1);
                    if (token != null
                            && token.type == Token.MARKUP_TAG_DELIMITER
                            && ">".equals(token.getLexeme())) {
                        Token next = token.getNextToken();
                        if (next != null
                                && next.type == Token.MARKUP_TAG_DELIMITER
                                && "</".equals(next.getLexeme())) {
                            canSpecialIndent = true;
                    if (canSpecialIndent) {
                        sb.append(leadingWS); // an extra intednted newline
                        sta.replaceRange(sb.toString(), caretPos, end);
                        sta.setCaretPosition(caretPos + leadingWS.length() + 1);
                        sta.replaceSelection("\t"); // further indent caret
                    } else {
                        sta.replaceRange(sb.toString(), caretPos, end);
                        sta.setCaretPosition(caretPos + leadingWS.length() + 1);
                if (sta.getShouldIndentNextLine(lineNum)) {

            } catch (BadLocationException ble) { // Never happens

        public String getMacroID() {
            return this.getName();

To be used this way:
Code: Select all
String style = "text/xml-custom";
AbstractTokenMakerFactory atmf =
(AbstractTokenMakerFactory) TokenMakerFactory.getDefaultInstance();

Dunno if this is the right way of doing things but it works for me ATM.
Posts: 27
Joined: Wed Jan 25, 2012 10:09 am

Return to Help

Who is online

Users browsing this forum: No registered users and 4 guests