diff --git a/java/java.editor/src/org/netbeans/modules/editor/java/JavaBracesMatcher.java b/java/java.editor/src/org/netbeans/modules/editor/java/JavaBracesMatcher.java index 065880bfa694..7c8bddf7985b 100644 --- a/java/java.editor/src/org/netbeans/modules/editor/java/JavaBracesMatcher.java +++ b/java/java.editor/src/org/netbeans/modules/editor/java/JavaBracesMatcher.java @@ -21,9 +21,11 @@ import com.sun.source.tree.IfTree; import com.sun.source.tree.StatementTree; import com.sun.source.tree.Tree; +import static com.sun.source.tree.Tree.Kind.CLASS; import com.sun.source.util.TreePath; import java.io.IOException; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.text.AbstractDocument; import javax.swing.text.BadLocationException; import javax.swing.text.Document; @@ -50,21 +52,26 @@ */ public final class JavaBracesMatcher implements BracesMatcher, BracesMatcherFactory, BracesMatcher.ContextLocator { - private static final char [] PAIRS = new char [] { '(', ')', '[', ']', '{', '}' }; //NOI18N - private static final JavaTokenId [] PAIR_TOKEN_IDS = new JavaTokenId [] { - JavaTokenId.LPAREN, JavaTokenId.RPAREN, - JavaTokenId.LBRACKET, JavaTokenId.RBRACKET, - JavaTokenId.LBRACE, JavaTokenId.RBRACE - }; - + private static final char[] PAIRS = new char[]{'(', ')', '[', ']', '{', '}', '<', '>'}; //NOI18N + private static final JavaTokenId[][] PAIR_TOKEN_IDS = { + new JavaTokenId[]{JavaTokenId.LPAREN}, + new JavaTokenId[]{JavaTokenId.RPAREN}, + new JavaTokenId[]{JavaTokenId.LBRACKET}, + new JavaTokenId[]{JavaTokenId.RBRACKET}, + new JavaTokenId[]{JavaTokenId.LBRACE}, + new JavaTokenId[]{JavaTokenId.RBRACE}, + new JavaTokenId[]{JavaTokenId.LT}, + new JavaTokenId[]{JavaTokenId.GT, JavaTokenId.GTGT, JavaTokenId.GTGTGT}}; + private static final JavaTokenId[] GENERIC_CANDIDATE_TOKEN_IDS = {JavaTokenId.LT, JavaTokenId.GT, JavaTokenId.GTGT, JavaTokenId.GTGTGT}; + private final MatcherContext context; - + private int originOffset; private char originChar; private char matchingChar; private boolean backward; private List> sequences; - + public JavaBracesMatcher() { this(null); } @@ -72,30 +79,30 @@ public JavaBracesMatcher() { private JavaBracesMatcher(MatcherContext context) { this.context = context; } - + private JavaBracesMatcher(MatcherContext context, int searchOffset) { this(context); this.searchOffset = searchOffset; } - + private int searchOffset = -1; - + private int getSearchOffset() { return searchOffset >=0 ? searchOffset : context.getSearchOffset(); } - + // ----------------------------------------------------- // BracesMatcher implementation // ----------------------------------------------------- - + @Override public int[] findOrigin() throws BadLocationException, InterruptedException { ((AbstractDocument) context.getDocument()).readLock(); try { int [] origin = BracesMatcherSupport.findChar( - context.getDocument(), - getSearchOffset(), - context.getLimitOffset(), + context.getDocument(), + getSearchOffset(), + context.getLimitOffset(), PAIRS ); @@ -121,7 +128,14 @@ public int[] findOrigin() throws BadLocationException, InterruptedException { } } } - + if (matchingChar == '<' || + matchingChar == '>') { + // test if matchingChar is part of java generic. + boolean valid = isPartOfGeneric(context.getDocument(), originOffset); + if (!valid) { + return null; + } + } return new int [] { originOffset, originOffset + 1 }; } else { return null; @@ -139,20 +153,42 @@ public int[] findMatches() throws InterruptedException, BadLocationException { TokenSequence seq = sequences.get(sequences.size() - 1); TokenHierarchy th = TokenHierarchy.get(context.getDocument()); - List> list; - if (backward) { - list = th.tokenSequenceList(seq.languagePath(), 0, originOffset); - } else { - list = th.tokenSequenceList(seq.languagePath(), originOffset + 1, context.getDocument().getLength()); - } + List> list = null; int counter = 0; - seq.move(originOffset); - if (seq.moveNext()) { + boolean seqMoved = seq.moveNext(); + if (seqMoved) { + Token token = seq.token(); + if (containsToken(token.id(), GENERIC_CANDIDATE_TOKEN_IDS)) { + // for a token with length > 1 we need to change the token sequence list because we want to process GTGT or GTGTGT tokens only once. + // Also we need to change the counter for a multichar token because we need to consider a cursor position within the multichar token + if (token.id().equals(JavaTokenId.GTGT)) { + if (backward) { + list = th.tokenSequenceList(seq.languagePath(), 0, originOffset - token.length() + 1); + } + counter += originOffset - seq.offset(); + } else if (token.id().equals(JavaTokenId.GTGTGT)) { + if (backward) { + list = th.tokenSequenceList(seq.languagePath(), 0, originOffset - token.length() + 1); + } + counter += originOffset - seq.offset(); + } + } + + } + if (list == null) { + // create token sequence list for all tokens except GTGT and GTGTGT + if (backward) { + list = th.tokenSequenceList(seq.languagePath(), 0, originOffset); + } else { + list = th.tokenSequenceList(seq.languagePath(), originOffset + 1, context.getDocument().getLength()); + } + } + if (seqMoved) { Token token = seq.token(); if (token.id() == JavaTokenId.STRING_LITERAL || - token.id() == JavaTokenId.MULTILINE_STRING_LITERAL) { + token.id() == JavaTokenId.MULTILINE_STRING_LITERAL) { for(TokenSequenceIterator tsi = new TokenSequenceIterator(list, backward); tsi.hasMore(); ) { TokenSequence sq = tsi.getSequence(); @@ -243,40 +279,120 @@ public int[] findMatches() throws InterruptedException, BadLocationException { } } } - return null; + return null; } } - JavaTokenId originId = getTokenId(originChar); - JavaTokenId lookingForId = getTokenId(matchingChar); + JavaTokenId[] originId = getTokenId(originChar); + JavaTokenId[] lookingForId = getTokenId(matchingChar); for(TokenSequenceIterator tsi = new TokenSequenceIterator(list, backward); tsi.hasMore(); ) { TokenSequence sq = tsi.getSequence(); - - if (originId == sq.token().id()) { - counter++; - } else if (lookingForId == sq.token().id()) { - if (counter == 0) { - matchStart = sq.offset(); - return new int [] { sq.offset(), sq.offset() + sq.token().length() }; + if (containsToken(sq.token().id(), originId)) { + if (containsToken(sq.token().id(), GENERIC_CANDIDATE_TOKEN_IDS)) { + if (sq.token().id().equals(JavaTokenId.GTGT)) { + counter += 2; + } else if (sq.token().id().equals(JavaTokenId.GTGTGT)) { + counter += 3; + } else { + counter++; + } + } else { + counter++; + } + } else if (containsToken(sq.token().id(), lookingForId)) { + if (containsToken(sq.token().id(), GENERIC_CANDIDATE_TOKEN_IDS)) { + boolean multipledGT = false; + if (counter == 0) { + matchStart = sq.offset(); + return new int[]{sq.offset(), sq.offset() + 1}; + } else { + if (sq.token().id().equals(JavaTokenId.GTGT)) { + multipledGT = true; + counter -= 2; + } else if (sq.token().id().equals(JavaTokenId.GTGTGT)) { + multipledGT = true; + counter -= 3; + } else { + counter--; + } + // we need to check the counter again for a multichar token >> or >>> because we can have the counter < 0 + // i.e for example Map>> will have counter = -1 + if (multipledGT && counter <= 0) { + matchStart = sq.offset(); + return new int[]{sq.offset() + sq.token().length() + counter, sq.offset() + sq.token().length() + counter + 1}; + } + } } else { - counter--; + if (counter == 0) { + matchStart = sq.offset(); + return new int[]{sq.offset(), sq.offset() + sq.token().length()}; + } else { + counter--; + } } + } } } - return null; } finally { ((AbstractDocument) context.getDocument()).readUnlock(); } } - + + /** + * Check if token is java generic. + * + * @param doc + * @param srcOffset + * @return true if token is java generic. + */ + private boolean isPartOfGeneric(Document doc, final int srcOffset) { + JavaSource javaSource = JavaSource.forDocument(doc); + if (javaSource == null) { + return false; + } + final AtomicBoolean valid = new AtomicBoolean(); + try { + javaSource.runUserActionTask(new Task() { + @Override + public void run(CompilationController ctrl) throws Exception { + ctrl.toPhase(JavaSource.Phase.PARSED); + TreePath path = ctrl.getTreeUtilities().pathFor(srcOffset); + if (path == null) { + return; + } + switch (path.getLeaf().getKind()) { + case GREATER_THAN: + case GREATER_THAN_EQUAL: + case LESS_THAN: + case LESS_THAN_EQUAL: + case LEFT_SHIFT: + case LEFT_SHIFT_ASSIGNMENT: + case RIGHT_SHIFT: + case RIGHT_SHIFT_ASSIGNMENT: + case UNSIGNED_RIGHT_SHIFT: + case UNSIGNED_RIGHT_SHIFT_ASSIGNMENT: + // ignore logical and aritmetic operations + return; + default: + valid.set(true); + break; + } + } + }, true); + } catch (IOException ex) { + // ignore exception, false will be returned + } + return valid.get(); + } + /** * Start of the matched brace/bracket */ private int matchStart; - + /** * Provides better context if the matched counterpart character is the opening curly brace. */ @@ -291,14 +407,14 @@ public BraceContext findContext(int originOrMatchPosition) { } return null; } - + public BraceContext findContextBackwards(final int p2) throws BadLocationException, IOException { // sanity check, do not accept anything but the original offset for now. if (p2 != originOffset) { return null; } final int position = matchStart; - + JavaSource src = JavaSource.forDocument(context.getDocument()); if (src == null) { return null; @@ -320,7 +436,7 @@ public void run(CompilationController ctrl) throws Exception { } else { block = null; } - + switch (path.getLeaf().getKind()) { case IF: { IfTree ifTree = (IfTree)path.getLeaf(); @@ -328,7 +444,7 @@ public void run(CompilationController ctrl) throws Exception { if (block == ifTree.getElseStatement()) { // the related region is the if statement up to the 'then' statement final int[] elseStart = { (int)ctrl.getTrees().getSourcePositions().getStartPosition( - ctrl.getCompilationUnit(), ifTree.getElseStatement())}; + ctrl.getCompilationUnit(), ifTree.getElseStatement())}; // must use lexer to iterate backwards from block start to 'else' keyword. The keyword position // is not a part of the Tree @@ -354,9 +470,9 @@ public void run() { }); // the context is the else statement up to the brace position int ifStart = (int)ctrl.getTrees().getSourcePositions().getStartPosition( - ctrl.getCompilationUnit(), ifTree); + ctrl.getCompilationUnit(), ifTree); int ifEnd; - + if (ifTree.getThenStatement().getKind() == Tree.Kind.BLOCK) { ifEnd = (int)ctrl.getTrees().getSourcePositions().getStartPosition( ctrl.getCompilationUnit(), ifTree.getThenStatement()); @@ -365,24 +481,24 @@ public void run() { ctrl.getCompilationUnit(), ifTree.getCondition()); } BraceContext rel = BraceContext.create( - context.getDocument().createPosition(ifStart), - context.getDocument().createPosition(ifEnd + 1)); + context.getDocument().createPosition(ifStart), + context.getDocument().createPosition(ifEnd + 1)); ret[0] = rel.createRelated( - context.getDocument().createPosition(elseStart[0]), - context.getDocument().createPosition(position + 1)); + context.getDocument().createPosition(elseStart[0]), + context.getDocument().createPosition(position + 1)); return; } } // fall through case SWITCH: - case WHILE_LOOP: + case WHILE_LOOP: case METHOD: case NEW_CLASS: case CASE: { // take start of the command as the context long start = ctrl.getTrees().getSourcePositions().getStartPosition( - ctrl.getCompilationUnit(), path.getLeaf()); + ctrl.getCompilationUnit(), path.getLeaf()); ret[0] = BraceContext.create( context.getDocument().createPosition((int)start), context.getDocument().createPosition(position)); @@ -391,7 +507,7 @@ public void run() { case CLASS: { long start = ctrl.getTrees().getSourcePositions().getStartPosition( - ctrl.getCompilationUnit(), block != null ? block : path.getLeaf()); + ctrl.getCompilationUnit(), block != null ? block : path.getLeaf()); ret[0] = BraceContext.create( context.getDocument().createPosition((int)start), context.getDocument().createPosition(position)); @@ -399,19 +515,19 @@ public void run() { } default: return; - + } } }, true); - + return ret[0]; } - + // ----------------------------------------------------- // private implementation // ----------------------------------------------------- - - private JavaTokenId getTokenId(char ch) { + + private JavaTokenId[] getTokenId(char ch) { for(int i = 0; i < PAIRS.length; i++) { if (PAIRS[i] == ch) { return PAIR_TOKEN_IDS[i]; @@ -419,7 +535,7 @@ private JavaTokenId getTokenId(char ch) { } return null; } - + public static List> getEmbeddedTokenSequences( TokenHierarchy th, int offset, boolean backwardBias, Language language ) { @@ -433,23 +549,23 @@ public static List> getEmbeddedTokenSequences( sequences.remove(i); } } - + return sequences; } - + private static final class TokenSequenceIterator { - + private final List> list; private final boolean backward; - + private int index; - + public TokenSequenceIterator(List> list, boolean backward) { this.list = list; this.backward = backward; this.index = -1; } - + public boolean hasMore() { return backward ? hasPrevious() : hasNext(); } @@ -458,64 +574,76 @@ public TokenSequence getSequence() { assert index >= 0 && index < list.size() : "No sequence available, call hasMore() first."; //NOI18N return list.get(index); } - + private boolean hasPrevious() { boolean anotherSeq = false; - + if (index == -1) { index = list.size() - 1; anotherSeq = true; } - + for( ; index >= 0; index--) { TokenSequence seq = list.get(index); if (anotherSeq) { seq.moveEnd(); } - + if (seq.movePrevious()) { return true; } - + anotherSeq = true; } - + return false; } - + private boolean hasNext() { boolean anotherSeq = false; - + if (index == -1) { index = 0; anotherSeq = true; } - + for( ; index < list.size(); index++) { TokenSequence seq = list.get(index); if (anotherSeq) { seq.moveStart(); } - + if (seq.moveNext()) { return true; } - + anotherSeq = true; } - + return false; } } // End of TokenSequenceIterator class - + // ----------------------------------------------------- // BracesMatcherFactory implementation // ----------------------------------------------------- - + /** */ @Override public BracesMatcher createMatcher(MatcherContext context) { return new JavaBracesMatcher(context); } + private boolean containsToken(TokenId token, JavaTokenId... tokens) { + if (tokens == null) { + return false; + } + for (int i = 0; i < tokens.length; i++) { + if (token.equals(tokens[i])) { + return true; + } + } + return false; + } + } diff --git a/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/JavaBracesMatcherTest.java b/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/JavaBracesMatcherTest.java index ca6f94bcf88f..f293764a0442 100644 --- a/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/JavaBracesMatcherTest.java +++ b/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/JavaBracesMatcherTest.java @@ -18,46 +18,129 @@ */ package org.netbeans.modules.editor.java; +import java.nio.file.Paths; import javax.swing.text.BadLocationException; +import javax.swing.text.Document; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertNotNull; +import org.junit.Test; import org.netbeans.api.java.lexer.JavaTokenId; +import org.netbeans.api.java.source.JavaSource; +import org.netbeans.api.java.source.SourceUtilsTestUtil; +import org.netbeans.api.java.source.SourceUtilsTestUtil2; +import org.netbeans.api.java.source.TestUtilities; +import org.netbeans.api.lexer.Language; import org.netbeans.editor.BaseDocument; import org.netbeans.junit.NbTestCase; import org.netbeans.modules.editor.bracesmatching.api.BracesMatchingTestUtils; import org.netbeans.spi.editor.bracesmatching.BracesMatcher; import org.netbeans.spi.editor.bracesmatching.BracesMatcherFactory; import org.netbeans.spi.editor.bracesmatching.MatcherContext; +import org.openide.cookies.EditorCookie; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.loaders.DataObject; /** * */ public class JavaBracesMatcherTest extends NbTestCase { + private volatile int testNumber = 0; + public JavaBracesMatcherTest(String name) { super(name); } + @Override + protected void setUp() throws Exception { + SourceUtilsTestUtil.prepareTest(new String[]{"org/netbeans/modules/java/editor/resources/layer.xml"}, new Object[]{}); + SourceUtilsTestUtil2.disableArtificalParameterNames(); + } + + @Test public void testStringTemplateBrackets() throws Exception { assertMatches2("\"\\^{test^}\""); } + @Test public void testMultilineStringBrackets() throws Exception { assertMatches2( - "\"\"\"\n" - + "^(\n" - + "^)\n" - + "\"\"\""); + "\"\"\"\n" + + "^(\n" + + "^)\n" + + "\"\"\""); + } + + @Test + public void testAngleBrackets() throws Exception { + perfomAngleBracketsTest("Map x = new HashMap^^>()"); + perfomAngleBracketsTest("Map x = new HashMap>()"); + perfomAngleBracketsTest("Map x = new HashMap^>^>()"); + perfomAngleBracketsTest("Map x = new HashMap^>>()"); + perfomAngleBracketsTest("Map x = new HashMap^ > ^>()"); + perfomAngleBracketsTest("Map x = new HashMap ^> >()"); + perfomAngleBracketsTest("Map x = new HashMap^ > ^>()"); + perfomAngleBracketsTest("Map x = new HashMap^>>>^>()"); + perfomAngleBracketsTest("Map x = new HashMap>>^>>()"); + perfomAngleBracketsTest("Map x = new HashMap>^>>>()"); + perfomAngleBracketsTest("Map x = new HashMap^>>>>()"); + perfomAngleBracketsTest(""" + Map> x = new HashMap>() + Object a = x.^>^>get(0) + """); + } + + private String makeTestClass(String angleStr) { + String ret = "package text;\n" + + "import java.util.List;\n" + + "import java.util.Map;\n" + + "import java.util.HashMap;\n" + + "public class Test" + testNumber + " {\n" + + "public void test() {\n" + + angleStr + ";\n" + + "}\n" + + "}\n"; + + return ret; + } + + private void perfomAngleBracketsTest(String angleStr) throws Exception { + testNumber++; + String srcTmp = makeTestClass(angleStr); + int caretPos = srcTmp.indexOf('^'); + String sourceCode = srcTmp.substring(0, caretPos) + srcTmp.substring(caretPos + 1); + int matchingCaretPos = sourceCode.indexOf('^'); + sourceCode = sourceCode.substring(0, matchingCaretPos) + sourceCode.substring(matchingCaretPos + 1); + FileObject wd = FileUtil.toFileObject(getWorkDir()); + FileObject sourceDir = FileUtil.createFolder(wd, "src"); + FileObject buildDir = FileUtil.createFolder(wd, "build"); + FileObject cacheFolder = FileUtil.createFolder(wd, "cache"); + Paths.get(cacheFolder.toURI()).toFile().mkdirs(); + FileObject testFO = FileUtil.createData(sourceDir, "test/Test" + testNumber + ".java"); + TestUtilities.copyStringToFile(testFO, sourceCode); + SourceUtilsTestUtil.prepareTest(sourceDir, buildDir, cacheFolder); + JavaSource source = JavaSource.forFileObject(testFO); + assertNotNull(source); + DataObject od = DataObject.find(testFO); + EditorCookie ec = od.getCookie(EditorCookie.class); + Document doc = ec.openDocument(); + doc.putProperty(Language.class, JavaTokenId.language()); + doc.putProperty("mimeType", JavaKit.JAVA_MIME_TYPE); + computeAndAssertMatches(doc, caretPos, false, matchingCaretPos); + computeAndAssertMatches(doc, caretPos + 1, true, matchingCaretPos); + computeAndAssertMatches(doc, matchingCaretPos, false, caretPos); + computeAndAssertMatches(doc, matchingCaretPos + 1, true, caretPos); } //from CslTestBase: protected void assertMatches2(String original) throws Exception { int caretPos = original.indexOf('^'); - original = original.substring(0, caretPos) + original.substring(caretPos+1); + original = original.substring(0, caretPos) + original.substring(caretPos + 1); int matchingCaretPos = original.indexOf('^'); - original = original.substring(0, matchingCaretPos) + original.substring(matchingCaretPos+1); + original = original.substring(0, matchingCaretPos) + original.substring(matchingCaretPos + 1); BaseDocument doc = getDocument(original); @@ -67,7 +150,7 @@ protected void assertMatches2(String original) throws Exception { computeAndAssertMatches(doc, matchingCaretPos + 1, true, caretPos); } - private void computeAndAssertMatches(BaseDocument doc, int pos, boolean backwards, int matchingPos) throws BadLocationException, InterruptedException { + private void computeAndAssertMatches(Document doc, int pos, boolean backwards, int matchingPos) throws BadLocationException, InterruptedException { BracesMatcherFactory factory = new JavaBracesMatcher(); MatcherContext context = BracesMatchingTestUtils.createMatcherContext(doc, pos, backwards, 1); BracesMatcher matcher = factory.createMatcher(context); @@ -86,13 +169,11 @@ private void computeAndAssertMatches(BaseDocument doc, int pos, boolean backward } private BaseDocument getDocument(String content) throws Exception { - BaseDocument doc = new BaseDocument(true, "text/x-java") { + BaseDocument doc = new BaseDocument(true, JavaKit.JAVA_MIME_TYPE) { }; - doc.putProperty(org.netbeans.api.lexer.Language.class, JavaTokenId.language()); - doc.insertString(0, content, null); - return doc; } + }