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!
Okay, so I'm assuming that if you comment out the listener.codeInserted(start, end, text); line in your overridden replaceText method then selection works as normal, without the need to click twice.
I guess the reason you want to clear the bracket _here_ is because the text modification is going to mess up pair.start and pair.end, if that is the case why not just modify their values if needed based on the replaceText parameters ?
I tried the approach you mentioned and it works when typing slowly.
The issue is, if the rate of text change (say someone presses down the bracket key) is greater than the speed of processing then multiple old brackets' highlighting is not cleared.
Also, at times, this can lead to concurrent modification exceptions. Although concurrent modification can be taken care by synchronization, which I tried, this does not guarantee which thread will execute first, leading to wilder complications.
This is compounded by the fact that I am also implementing bracket auto-completion.
If you don't mind posting a simple but complete program with just your bracket highlighting and auto complete sections then I'll have a look.
I have prepared a short demo as requested here: https://github.com/prat-man/CodeAreaBracketsDemo
I have stripped everything else (including syntax highlighting, etc.) and only one type of brackets [ ] are supported to keep things simple.
It is a maven based IntelliJ project. Please have a look. Thanks!
Here are the two main classes posted here directly for a quicker access:
Controller.java
package in.pratanumandal.demo;
import javafx.fxml.FXML;
import javafx.scene.layout.BorderPane;
public class Controller {
@FXML private BorderPane mainPane;
@FXML
public void initialize() {
// create a new code area
CustomCodeArea codeArea = new CustomCodeArea();
// add code area to main pane
mainPane.setCenter(codeArea);
// highlight brackets
BracketHighlighter bracketHighlighter = new BracketHighlighter(codeArea);
// auto complete loops
codeArea.setOnKeyTyped(keyEvent -> {
// clear bracket highlighting
bracketHighlighter.clearBracket();
// get typed character
String character = keyEvent.getCharacter();
// add a ] if [ is typed
if (character.equals("[")) {
int position = codeArea.getCaretPosition();
codeArea.insert(position, "]", "loop");
codeArea.moveTo(position);
}
// remove next ] if ] is typed
else if (character.equals("]")) {
int position = codeArea.getCaretPosition();
if (position != codeArea.getLength()) {
String nextChar = codeArea.getText(position, position + 1);
if (nextChar.equals("]")) codeArea.deleteText(position, position + 1);
}
}
// refresh bracket highlighting
bracketHighlighter.highlightBracket();
});
}
}
BracketHighlighter.java
package in.pratanumandal.demo;
import java.util.*;
public class BracketHighlighter {
// the code area
private final CustomCodeArea codeArea;
// the map of bracket pairs existing in code
private Map<Integer, Integer> brackets;
// the list of highlighted bracket pairs
private List<BracketPair> bracketPairs;
/**
* Parameterized constructor
* @param codeArea the code area
*/
public BracketHighlighter(CustomCodeArea codeArea) {
this.codeArea = codeArea;
this.brackets = new HashMap<>();
this.bracketPairs = new ArrayList<>();
this.codeArea.addTextInsertionListener((start, end, text) -> clearBracket());
this.codeArea.textProperty().addListener((obs, oldVal, newVal) -> initializeBrackets(newVal));
this.codeArea.caretPositionProperty().addListener((obs, oldVal, newVal) -> highlightBracket(newVal));
}
/**
* Method to initialize the bracket pairs (do only when text is changed)
*
* @param code the next text
*/
private synchronized void initializeBrackets(String code) {
// clear bracket map
this.brackets.clear();
// compute matching brackets and add to map
Stack<Integer> stack = new Stack<>();
int index = 0;
while (index < code.length()) {
int i = code.indexOf("[", index);
int j = code.indexOf("]", index);
if (i == -1 && j == -1) {
break;
} else if (i != -1 && (i < j || j == -1)) {
stack.push(i);
index = i + 1;
} else if (j != -1) {
if (!stack.isEmpty()) {
int k = stack.pop();
brackets.put(k, j);
brackets.put(j, k);
}
index = j + 1;
}
}
}
/**
* Highlight the matching bracket at new caret position
*
* @param newVal the new caret position
*/
private synchronized 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 = this.brackets.get(newVal);
if (other != null) {
// other half exists
BracketPair pair = new BracketPair(newVal, other);
// highlight start
if (pair.start < codeArea.getLength()) {
String text = codeArea.getText(pair.start, pair.start + 1);
if (text.equals("[") || text.equals("]")) {
List<String> styleList = new ArrayList<>();
styleList.add("loop");
styleList.add("match");
// if we run the following line with Platform.runLater, selection works
// but bracket highlighting breaks down
codeArea.setStyle(pair.start, pair.start + 1, styleList);
}
}
// highlight end
if (pair.end < codeArea.getLength()) {
String text = codeArea.getText(pair.end, pair.end + 1);
if (text.equals("[") || text.equals("]")) {
List<String> styleList = new ArrayList<>();
styleList.add("loop");
styleList.add("match");
// if we run the following line with Platform.runLater, selection works
// but bracket highlighting breaks down
codeArea.setStyle(pair.end, pair.end + 1, styleList);
}
}
// add bracket pair to list
this.bracketPairs.add(pair);
}
}
/**
* Highlight the matching bracket at current caret position
*/
public synchronized void highlightBracket() {
this.highlightBracket(codeArea.getCaretPosition());
}
/**
* Clear the existing highlighted bracket styles
*/
public synchronized void clearBracket() {
Iterator<BracketPair> iterator = this.bracketPairs.iterator();
while (iterator.hasNext()) {
// get next bracket pair
BracketPair pair = iterator.next();
// clear start
if (pair.start < codeArea.getLength()) {
String text = codeArea.getText(pair.start, pair.start + 1);
if (text.equals("[") || text.equals("]")) {
List<String> styleList = new ArrayList<>();
styleList.add("loop");
// if we run the following line with Platform.runLater, selection works
// but bracket highlighting breaks down
codeArea.setStyle(pair.start, pair.start + 1, styleList);
}
}
// clear end
if (pair.end < codeArea.getLength()) {
String text = codeArea.getText(pair.end, pair.end + 1);
if (text.equals("[") || text.equals("]")) {
List<String> styleList = new ArrayList<>();
styleList.add("loop");
// if we run the following line with Platform.runLater, selection works
// but bracket highlighting breaks down
codeArea.setStyle(pair.end, pair.end + 1, styleList);
}
}
// remove bracket pair from list
iterator.remove();
}
}
/**
* 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 +
'}';
}
}
}
Great, not sure if I'll get to it today though so probably tomorrow ....
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 +
'}';
}
}
}
Thanks a lot for helping me out!
Changing it to codeArea.caretPositionProperty().addListener((obs,old,newVal) -> Platform.runLater(() -> highlightBracket(newVal))); fixes the problem and even after testing with multiple scenarios, I still haven't found any issues.
Also thanks for the improved code, I have adopted some parts of it like the styleBrackets(BracketPair pair, List<String> styles) and styleBracket(int pos, List<String> styles) methods. They have really cleaned up my code.
However, I am staying with the approach of computing the brackets all at once, since it is more efficient for my use case.
Also, I have created a pull request with an improved demo supporting multiple bracket types at https://github.com/FXMisc/RichTextFX/pull/959.
I think that would be a really useful addition for anyone trying to do bracket highlighting using RichTextFX.
You're welcome, thanks for PR.
Most helpful comment
You're welcome, thanks for PR.