/* * License: source-license.txt * If this code is used independently, copy the license here. */ package wombat.gui.text; import java.util.ArrayList; import java.util.List; import java.util.Stack; import javax.swing.event.CaretListener; import javax.swing.event.CaretEvent; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultHighlighter; import javax.swing.text.Highlighter; import javax.swing.text.StyleConstants; import javax.swing.text.Utilities; import wombat.gui.frames.MainFrame; import wombat.util.errors.ErrorManager; /** * Used to highlight matching brackets (both square and rounded, but not angle * or curly, although those could be added easily enough. */ public class BracketMatcher implements CaretListener { // The text area that SchemeTextArea textArea; // If we can't match brackets enough times, disable them. static boolean disabled = false; // Any active brackets are moved before each new highlight. List<Object> activeTags = new ArrayList<Object>(); /** * Create a bracket matcher for a given text area. * * @param text */ public BracketMatcher(SchemeTextArea text) { textArea = text; } /** * When the caret moves, update the matched brackets. * * Also used to show the current line and column. TODO: Move this code to * its own object. * * @param event * Event parameters (ignored). */ public void caretUpdate(CaretEvent event) { // Update the current row and column of the caret. try { // Find the current row of the caret. int caretPos = textArea.code.getCaretPosition(); int rowNum = (caretPos == 0) ? 1 : 0; for (int offset = caretPos; offset > 0;) { offset = Utilities.getRowStart(textArea.code, offset) - 1; rowNum++; } // Use that to find the current column. int offset = Utilities.getRowStart(textArea.code, caretPos); int colNum = caretPos - offset + 1; MainFrame.Singleton().RowColumn.setText(rowNum + ":" + colNum); } // Can't find the caret correctly, reset the row:column indicator. catch (BadLocationException ex) { MainFrame.Singleton().RowColumn.setText("row:column"); } // If the bracket matcher has broken, don't keep trying. if (disabled) return; try { // Get the highlighter and remove all active tags. Highlighter h = textArea.code.getHighlighter(); for (Object tag : activeTags) h.removeHighlight(tag); activeTags.clear(); // Get the caret position. int pos = event.getDot() - 1; // If the caret is in the document and adjacent to a bracket. if (pos >= 0 && pos < textArea.getText().length() && "()[]".contains(textArea.code.getDocument().getText(pos, 1))) { // Get a direct link to the full text of the document to speed up future access. String text = textArea.code.getDocument().getText(0, textArea.code.getDocument().getLength()); // Skip character literals if (pos >= 2 && "#\\".equals(text.substring(pos - 2, pos))) return; // Skip if we're in a comment if ((text.lastIndexOf("#|", pos) > text.lastIndexOf("|#", pos)) || (text.lastIndexOf(';', pos) > text.lastIndexOf('\n', pos))) return; // Which way are we going? char orig, c; orig = c = text.charAt(pos); int matchPos, d = ((orig == '(' || orig == '[') ? 1 : -1); // Keep a stack of brackets so we find the correct level. Stack<Character> brackets = new Stack<Character>(); // Loop either forward or back depending on opening or closing initial bracket. int index; boolean foundMatch = false; for (matchPos = pos; matchPos >= 0 && matchPos < text.length(); matchPos += d) { // When moving towards the front: if (d < 0) { // Ignore line comments. index = text.lastIndexOf(';', matchPos); if (index >= 0 && index > text.lastIndexOf("\n", matchPos)) { matchPos = index; continue; } // Ignore block comments. index = text.lastIndexOf("#|", matchPos); if (index >= 0 && index < matchPos && matchPos < text.indexOf("|#")) { matchPos = index; continue; } } // When moving towards the end: else { // Ignore line comments. index = text.lastIndexOf(';', matchPos); if (index >= 0 && text.lastIndexOf('\n', matchPos) < index) { matchPos = text.indexOf('\n', matchPos); if (matchPos < 0) break; continue; } // Ignore block comments. index = text.indexOf("|#", matchPos); if (index >= 0 && text.lastIndexOf("#|", matchPos) < matchPos && matchPos < index) { matchPos = index; continue; } } // Get the current character. c = text.charAt(matchPos); // Ignore character literals if (matchPos >= 2 && "#\\".equals(text.substring(matchPos - 2, matchPos))) continue; // We're only done when we're at the correct level and find the correct bracket. if (!brackets.isEmpty() && brackets.peek() == c) { brackets.pop(); if (brackets.isEmpty()) { foundMatch = true; break; } } // If we still have brackets to deal with, make sure it's the correct kind. // If it's the incorrect kind, highligh with an error (default = red). else if (!brackets.isEmpty() && ((brackets.peek() == '(' && c == '[') || (brackets.peek() == '[' && c == '(') || (brackets.peek() == ')' && c == ']') || (brackets.peek() == ']' && c == ')'))) { foundMatch = false; break; } // Remember bracket level. else if (c == '(') brackets.push(')'); else if (c == ')') brackets.push('('); else if (c == '[') brackets.push(']'); else if (c == ']') brackets.push('['); } // Highlight it. Highlighter.HighlightPainter hp = new DefaultHighlighter.DefaultHighlightPainter( StyleConstants.getForeground(SchemeDocument.attributes.get(foundMatch ? "bracket" : "invalid-bracket"))); // Remember the bracket so we can remove it on the next cycle. try { activeTags.add(h.addHighlight(pos, pos + 1, hp)); activeTags.add(h.addHighlight(matchPos, matchPos + 1, hp)); } catch (BadLocationException ble) { } } } // Ignore bad locations. This is usually empty documents. catch (BadLocationException ex) { } // If we get this, we broke the bracket matcher. Remember the error and disable it for the future. // This shouldn't happen unless things go badly wrong. catch (Exception ex) { ErrorManager.logError("Unable to match paranthesis: " + ex.getMessage()); ex.printStackTrace(); disabled = true; } } }