question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

CodeArea selection does not work when changing style before text change

See original GitHub issue

I am trying to write a matching bracket highlighter for CodeArea. I have managed to get it working in the following manner:

I have extended CodeArea into CustomCodeArea. There I have overridden the following method:

@Override
public void replaceText(int start, int end, String text) {
    for (TextInsertionListener listener : insertionListeners) {
        listener.codeInserted(start, end, text);
    }
    super.replaceText(start, end, text);
}

Here the listener is used to listen for text change just before the text is actually changed. This is needed to clear old matching bracket highlights in the following manner: this.tabData.getCodeArea().addTextInsertionListener((start, end, text) -> clearBracket());

Inside the clear bracket method, I am removing the style in the following manner:

this.tabData.getCodeArea().setStyle(pair.start, pair.start + 1, styleList);
this.tabData.getCodeArea().setStyle(pair.end, pair.end + 1, styleList);

I must perform the operation of clearing the bracket in the main UX thread because it must be done before the text has changed.

When this is done, however, selection fails to work (selection highlight is not started, and nothing is selected) on the first try. I have to click at the same position once more to start the selection highlight.

If there is some way to fix this (maybe fire event manually, or something else), or there is a better approach to matching bracket highlighting for CodeArea, could you please help me with that.

Thanks a lot!

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:9 (4 by maintainers)

github_iconTop GitHub Comments

2reactions
Jugencommented, Sep 11, 2020

You’re welcome, thanks for PR.

2reactions
Jugencommented, Sep 9, 2020

First of all nice code and thanks for the code comments.

It turns out that the caretPositionProperty() listener, in the BracketHighlighter constructor, which calls highlightBracket( newVal ) somehow sabotages the area’s selection state. The simple solution is to just wrap that call in Platform.runLater like so:

codeArea.caretPositionProperty().addListener((obs,old,newVal) -> Platform.runLater(() -> highlightBracket(newVal)));

I took some time playing around with your code and also found that you don’t need to invoke bracketHighlighter.highlightBracket() in the Controller’s codeArea.setOnKeyTyped event handler because it will be invoked in any case via BracketHighlighter’s caretPositionProperty() listener.

Lastly I suggest an alternative to initializeBrackets which processes ALL the code area’s text every time an edit occurs. I think a better idea is to just search forward/backward from the current bracket to find it’s matching partner when needed. Here is my version of BracketHighlighter with some additional changes for you to assess and use if you like:

public class BracketHighlighter {

    private final CustomCodeArea codeArea;

    // the list of highlighted bracket pairs
    private List<BracketPair> bracketPairs = new ArrayList<>();

    // constants that don't need to be created every time
    private final List<String> LOOP_STYLE = Collections.singletonList( "loop" );
    private final List<String> MATCH_STYLE = Arrays.asList( "match", "loop" );
    private final String BRACKET_PAIRS = "(){}[]<>";

    /**
     * Parameterized constructor
     * @param codeArea the code area
     */
    public BracketHighlighter(CustomCodeArea codeArea) {
        this.codeArea = codeArea;
        this.codeArea.addTextInsertionListener((start, end, text) -> clearBracket());
        //this.codeArea.textProperty().addListener((obs, oldVal, newVal) -> initializeBrackets(newVal)); // Let's not process all the text every time ;-)
        this.codeArea.caretPositionProperty().addListener((obs, oldVal, newVal) -> Platform.runLater( () -> highlightBracket(newVal) ));
    }

    /**
     * Highlight the matching bracket at new caret position
     * @param newVal the new caret position
     */
    private void highlightBracket(int newVal) {

        // first clear existing bracket highlights
        this.clearBracket();

        // detect caret position both before and after bracket
        String prevChar = (newVal > 0) ? codeArea.getText(newVal - 1, newVal) : "";
        if (prevChar.equals("[") || prevChar.equals("]")) --newVal;

        // get other half of matching bracket
        Integer other = getMatchingBracket( newVal );

        if (other != null) {
            // other half exists
            BracketPair pair = new BracketPair(newVal, other);

            // highlight start and end
            styleBrackets( pair, MATCH_STYLE );

            // add bracket pair to list
            this.bracketPairs.add(pair);
        }
    }

    /**
     * Find the matching bracket location.
     * @param index to start searching from
     * @return null or position of matching bracket
     */
    private Integer getMatchingBracket( int index )
    {
        if ( index == codeArea.getLength() ) return null;

        char initialBracket = codeArea.getText( index, index+1 ).charAt(0);
        int bracketTypePosition = BRACKET_PAIRS.indexOf( initialBracket ); // "(){}[]<>"
        if ( bracketTypePosition < 0 ) return null;

        // even numbered bracketTypePositions are opening brackets, and odd positions are closing
        // if even (opening bracket) then step forwards, otherwise step backwards
        int stepDirection = ( bracketTypePosition % 2 == 0 ) ? +1 : -1;

        // the matching bracket to look for, the opposite of initialBracket
        char match = BRACKET_PAIRS.charAt( bracketTypePosition + stepDirection );

        index += stepDirection;
        int bracketCount = 1;

        while ( index > -1 && index < codeArea.getLength() ) {
            char code = codeArea.getText( index, index+1 ).charAt(0);
            if ( code == initialBracket ) bracketCount++;
            else if ( code == match ) bracketCount--;
            if ( bracketCount == 0 ) return index;
            else index += stepDirection;
        }

        return null;
    }

    /**
     * Highlight the matching bracket at current caret position
     */
    public void highlightBracket() {
        this.highlightBracket(codeArea.getCaretPosition());
    }

    /**
     * Clear the existing highlighted bracket styles
     */
    public void clearBracket() {

        Iterator<BracketPair> iterator = this.bracketPairs.iterator();

        while ( iterator.hasNext() )
        {
            // clear next bracket pair
            styleBrackets( iterator.next(), LOOP_STYLE );

            // remove bracket pair from list
            iterator.remove();
        }

    }

    private void styleBrackets( BracketPair pair, List<String> styles )
    {
        styleBracket( pair.start, styles );
        styleBracket( pair.end, styles );
    }

    private void styleBracket( int pos, List<String> styles )
    {
        if ( pos < codeArea.getLength() ) {
            String text = codeArea.getText( pos, pos + 1 );
            if ( text.equals("[") || text.equals("]") ) {
                codeArea.setStyle( pos, pos + 1, styles );
            }
        }
    }

    /**
     * Class representing a pair of matching bracket indices
     */
    static class BracketPair {

        private int start;
        private int end;

        public BracketPair(int start, int end) {
            this.start = start;
            this.end = end;
        }

        public int getStart() {
            return start;
        }

        public int getEnd() {
            return end;
        }

        @Override
        public String toString() {
            return "BracketPair{" +
                    "start=" + start +
                    ", end=" + end +
                    '}';
        }

    }

}
Read more comments on GitHub >

github_iconTop Results From Across the Web

Text selection and replacement not working properly in textarea
The problem is that you're setting the innerHTML of the textarea , you need to set value property of the text area. innerHTML...
Read more >
Starting a style at certain position in text (with having an index ...
Hi Tomas, I could not find a way to assign a style at the caret location. Is there one? What is available now...
Read more >
selection - CSS: Cascading Style Sheets - MDN Web Docs
The ::selection CSS pseudo-element applies styles to the part of a document that has been highlighted by the user (such as clicking and ......
Read more >
Control data entry formats with input masks - Microsoft Support
An input mask only affects whether Access accepts the data – the mask does not change how the data is stored, which is...
Read more >
ion-input: Custom Input Value Type Styling and CSS Properties
Labels will take up the width of their content by default. This positioning can be changed to be a fixed width, stacked, or...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found