You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2019/01/04 17:19:52 UTC

[groovy] branch master updated: GROOVY-8942: Highlight matched parentheses, brackets and curly braces in Groovy Console when caret touching them(closes #847)

This is an automated email from the ASF dual-hosted git repository.

sunlan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new 61e42df  GROOVY-8942: Highlight matched parentheses, brackets and curly braces in Groovy Console when caret touching them(closes #847)
61e42df is described below

commit 61e42df90d757dd72e05e6b05a1b8253abaef441
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sat Jan 5 01:18:27 2019 +0800

    GROOVY-8942: Highlight matched parentheses, brackets and curly braces in Groovy Console when caret touching them(closes #847)
---
 .../main/groovy/groovy/ui/ConsoleTextEditor.java   |  32 ++-
 .../groovy/groovy/ui/text/MatchingHighlighter.java | 221 +++++++++++++++++++++
 .../groovy/groovy/ui/text/SmartDocumentFilter.java |  18 +-
 3 files changed, 266 insertions(+), 5 deletions(-)

diff --git a/subprojects/groovy-console/src/main/groovy/groovy/ui/ConsoleTextEditor.java b/subprojects/groovy-console/src/main/groovy/groovy/ui/ConsoleTextEditor.java
index a4e9c8a..41fcccf 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/ui/ConsoleTextEditor.java
+++ b/subprojects/groovy-console/src/main/groovy/groovy/ui/ConsoleTextEditor.java
@@ -19,19 +19,33 @@
 package groovy.ui;
 
 import groovy.ui.text.GroovyFilter;
+import groovy.ui.text.MatchingHighlighter;
+import groovy.ui.text.SmartDocumentFilter;
 import groovy.ui.text.StructuredSyntaxResources;
 import groovy.ui.text.TextEditor;
 import groovy.ui.text.TextUndoManager;
 import org.codehaus.groovy.runtime.StringGroovyMethods;
 
-import javax.swing.*;
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.ActionMap;
+import javax.swing.InputMap;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.KeyStroke;
+import javax.swing.event.CaretListener;
 import javax.swing.event.DocumentEvent;
 import javax.swing.event.DocumentListener;
 import javax.swing.text.BadLocationException;
 import javax.swing.text.DefaultStyledDocument;
 import javax.swing.text.Document;
 import javax.swing.text.DocumentFilter;
-import java.awt.*;
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Point;
 import java.awt.datatransfer.DataFlavor;
 import java.awt.datatransfer.Transferable;
 import java.awt.event.ActionEvent;
@@ -314,7 +328,19 @@ public class ConsoleTextEditor extends JScrollPane {
         DefaultStyledDocument doc = (DefaultStyledDocument) textEditor.getDocument();
 
         try {
-            doc.setDocumentFilter((DocumentFilter) clazz.getConstructor(doc.getClass()).newInstance(doc));
+            DocumentFilter documentFilter = (DocumentFilter) clazz.getConstructor(doc.getClass()).newInstance(doc);
+            doc.setDocumentFilter(documentFilter);
+
+            if (documentFilter instanceof SmartDocumentFilter) {
+                final SmartDocumentFilter smartDocumentFilter = (SmartDocumentFilter) documentFilter;
+
+                for (CaretListener cl : textEditor.getCaretListeners()) {
+                    if (cl instanceof MatchingHighlighter) {
+                        textEditor.removeCaretListener(cl);
+                    }
+                }
+                textEditor.addCaretListener(new MatchingHighlighter(smartDocumentFilter, textEditor));
+            }
         } catch (ReflectiveOperationException e) {
             e.printStackTrace();
         }
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/ui/text/MatchingHighlighter.java b/subprojects/groovy-console/src/main/groovy/groovy/ui/text/MatchingHighlighter.java
new file mode 100644
index 0000000..eec3a91
--- /dev/null
+++ b/subprojects/groovy-console/src/main/groovy/groovy/ui/text/MatchingHighlighter.java
@@ -0,0 +1,221 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.ui.text;
+
+import groovy.lang.Tuple2;
+import groovy.lang.Tuple3;
+import org.antlr.v4.runtime.Token;
+import org.apache.groovy.util.Maps;
+
+import javax.swing.JTextPane;
+import javax.swing.SwingUtilities;
+import javax.swing.event.CaretEvent;
+import javax.swing.event.CaretListener;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.DefaultStyledDocument;
+import javax.swing.text.Position;
+import javax.swing.text.Style;
+import javax.swing.text.StyleConstants;
+import javax.swing.text.StyleContext;
+import java.awt.Color;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+import static groovy.lang.Tuple.tuple;
+import static org.apache.groovy.parser.antlr4.GroovyLexer.LBRACE;
+import static org.apache.groovy.parser.antlr4.GroovyLexer.LBRACK;
+import static org.apache.groovy.parser.antlr4.GroovyLexer.LPAREN;
+import static org.apache.groovy.parser.antlr4.GroovyLexer.RBRACE;
+import static org.apache.groovy.parser.antlr4.GroovyLexer.RBRACK;
+import static org.apache.groovy.parser.antlr4.GroovyLexer.RPAREN;
+
+/**
+ * Represents highlighter to highlight matched parentheses, brackets and curly braces when caret touching them
+ *
+ * @since 3.0.0
+ */
+public class MatchingHighlighter implements CaretListener {
+    private final SmartDocumentFilter smartDocumentFilter;
+    private final JTextPane textEditor;
+    private final DefaultStyledDocument doc;
+    private static final Map<String, Tuple3<Integer, Integer, Boolean>> PAREN_MAP = Maps.of(
+            "(", tuple(LPAREN, RPAREN, true),
+            ")", tuple(RPAREN, LPAREN, false),
+            "[", tuple(LBRACK, RBRACK, true),
+            "]", tuple(RBRACK, LBRACK, false),
+            "{", tuple(LBRACE, RBRACE, true),
+            "}", tuple(RBRACE, LBRACE, false)
+    );
+    private volatile List<Tuple2<Integer, Position>> highlightedTokenInfoList = Collections.emptyList();
+
+    public MatchingHighlighter(SmartDocumentFilter smartDocumentFilter, JTextPane textEditor) {
+        this.smartDocumentFilter = smartDocumentFilter;
+        this.textEditor = textEditor;
+        this.doc = (DefaultStyledDocument) textEditor.getStyledDocument();
+
+        initStyles();
+    }
+
+    @Override
+    public void caretUpdate(CaretEvent e) {
+        highlight();
+    }
+
+    public void highlight() {
+        // `SwingUtilities.invokeLater` is used to avoid "java.lang.IllegalStateException: Attempt to mutate in notification"
+        SwingUtilities.invokeLater(this::doHighlight);
+    }
+
+    private void doHighlight() {
+        clearHighlighted();
+
+        if (!smartDocumentFilter.isLatest()) {
+            return;
+        }
+
+        int caretPosition = textEditor.getCaretPosition();
+        int f = -1;
+        String c = null;
+        try {
+            f = caretPosition - 1;
+            c = doc.getText(f, 1);
+        } catch (BadLocationException e1) {
+            // ignore
+        }
+
+        if (!PAREN_MAP.containsKey(c)) {
+            try {
+                f = caretPosition;
+                c = doc.getText(f, 1);
+            } catch (BadLocationException e1) {
+                // ignore
+            }
+        }
+
+        if (!PAREN_MAP.containsKey(c)) {
+            return;
+        }
+
+        final int offset = f;
+        final String p = c;
+
+        highlightMatched(offset, p);
+    }
+
+    private void highlightMatched(int offset, String p) {
+        List<Token> latestTokenList = smartDocumentFilter.getLatestTokenList();
+        Tuple3<Integer, Integer, Boolean> tokenTypeTuple = PAREN_MAP.get(p);
+        int triggerTokenType = tokenTypeTuple.getV1();
+        int matchedTokenType = tokenTypeTuple.getV2();
+        boolean normalOrder = tokenTypeTuple.getV3();
+        Deque<Tuple2<Token, Boolean>> stack = new ArrayDeque<>();
+
+        Token triggerToken = null;
+        Token matchedToken = null;
+
+        for (ListIterator<Token> iterator = latestTokenList.listIterator(normalOrder ? 0 : latestTokenList.size());
+             normalOrder ? iterator.hasNext() : iterator.hasPrevious(); ) {
+            Token token = normalOrder ? iterator.next() : iterator.previous();
+
+            int tokenType = token.getType();
+            if (tokenType == triggerTokenType) {
+                Boolean triggerFlag = offset == token.getStartIndex();
+
+                stack.push(tuple(token, triggerFlag));
+            } else if (tokenType == matchedTokenType) {
+                Tuple2<Token, Boolean> tokenAndTriggerFlagTuple = stack.pop();
+                if (tokenAndTriggerFlagTuple.getV2()) {
+                    triggerToken = tokenAndTriggerFlagTuple.getV1();
+                    matchedToken = token;
+                    break;
+                }
+            }
+        }
+
+        if (null != triggerToken && null != matchedToken) {
+            highlightToken(p, triggerToken);
+            highlightToken(p, matchedToken);
+            try {
+                highlightedTokenInfoList = Arrays.asList(
+                        tuple(triggerToken.getType(), doc.createPosition(triggerToken.getStartIndex())),
+                        tuple(matchedToken.getType(), doc.createPosition(matchedToken.getStartIndex()))
+                );
+            } catch (BadLocationException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private void initStyles() {
+        PAREN_MAP.keySet().forEach(e -> createHighlightedStyleByParen(e));
+    }
+
+    private final StyleContext styleContext = StyleContext.getDefaultStyleContext();
+    private final Style defaultStyle = styleContext.getStyle(StyleContext.DEFAULT_STYLE);
+
+    private void createHighlightedStyleByParen(String p) {
+        Style style = StyleContext.getDefaultStyleContext().addStyle(highlightedStyleName(p), findStyleByTokenType(PAREN_MAP.get(p).getV1()));
+        StyleConstants.setForeground(style, Color.YELLOW.darker());
+        StyleConstants.setBold(style, true);
+    }
+
+    private static String highlightedStyleName(String p) {
+        return "highlighted" + p;
+    }
+
+    private Style findHighlightedStyleByParen(String p) {
+        Style style = styleContext.getStyle(highlightedStyleName(p));
+
+        return null == style ? defaultStyle : style;
+    }
+
+    private Style findStyleByTokenType(int tokenType) {
+        Style style = styleContext.getStyle(String.valueOf(tokenType));
+
+        return null == style ? defaultStyle : style;
+    }
+
+    private void highlightToken(String p, final Token tokenToHighlight) {
+        Style style = findHighlightedStyleByParen(p);
+        doc.setCharacterAttributes(tokenToHighlight.getStartIndex(),
+                1,
+                style,
+                true);
+    }
+
+    private void clearHighlighted() {
+        if (!highlightedTokenInfoList.isEmpty()) {
+            for (Tuple2<Integer, Position> highlightedTokenInfo : highlightedTokenInfoList) {
+                doc.setCharacterAttributes(
+                        highlightedTokenInfo.getV2().getOffset(),
+                        1,
+                        findStyleByTokenType(highlightedTokenInfo.getV1()),
+                        true
+                );
+            }
+
+            highlightedTokenInfoList = Collections.emptyList();
+        }
+    }
+}
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/ui/text/SmartDocumentFilter.java b/subprojects/groovy-console/src/main/groovy/groovy/ui/text/SmartDocumentFilter.java
index c319baa..8539a2f 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/ui/text/SmartDocumentFilter.java
+++ b/subprojects/groovy-console/src/main/groovy/groovy/ui/text/SmartDocumentFilter.java
@@ -173,6 +173,7 @@ public class SmartDocumentFilter extends DocumentFilter {
             lexer = createLexer(styledDocument.getText(0, styledDocument.getLength()));
         } catch (IOException e) {
             e.printStackTrace();
+            this.latest = false;
             return;
         }
 
@@ -182,9 +183,11 @@ public class SmartDocumentFilter extends DocumentFilter {
             tokenStream.fill();
         } catch (LexerNoViableAltException | GroovySyntaxError e) {
             // ignore
+            this.latest = false;
             return;
         } catch (Exception e) {
             e.printStackTrace();
+            this.latest = false;
             return;
         }
 
@@ -221,6 +224,7 @@ public class SmartDocumentFilter extends DocumentFilter {
         }
 
         this.latestTokenList = tokenList;
+        this.latest = true;
     }
 
     private List<Token> findTokensToRender(List<Token> tokenList) {
@@ -338,9 +342,19 @@ public class SmartDocumentFilter extends DocumentFilter {
 
         // unexpected char, e.g. `
         Style unexpectedChar = createDefaultStyleByTokenType(UNEXPECTED_CHAR);
-        StyleConstants.setForeground(unexpectedChar, Color.YELLOW.darker().darker());
+        StyleConstants.setForeground(unexpectedChar, Color.CYAN.darker());
     }
 
-    private List<Token> latestTokenList = Collections.emptyList();
+    private volatile boolean latest = false;
+    private volatile List<Token> latestTokenList = Collections.emptyList();
     private static final String TAB_REPLACEMENT = "    ";
+
+    public boolean isLatest() {
+        return latest;
+    }
+
+    public List<Token> getLatestTokenList() {
+        return latestTokenList;
+    }
+
 }