From 0a419eb1c00115121dd911e1dc3d14a9fb43d5ee Mon Sep 17 00:00:00 2001 From: Petr Kudriavtsev Date: Fri, 12 Jan 2018 17:35:01 +0300 Subject: [PATCH] Implemented IDEABKL-4743 Automatic placement of --- .../editorActions/JavaTypedHandler.java | 89 ++++++++++++++++++- .../semicolonBeforeRightParenMoved_after.java | 30 +++++++ ...semicolonBeforeRightParenMoved_before.java | 30 +++++++ ...micolonBeforeRightParenNotMoved_after.java | 56 ++++++++++++ ...icolonBeforeRightParenNotMoved_before.java | 56 ++++++++++++ .../java/codeInsight/JavaTypingTest.java | 48 ++++++++++ 6 files changed, 307 insertions(+), 2 deletions(-) create mode 100644 java/java-tests/testData/codeInsight/typing/semicolonBeforeRightParenMoved_after.java create mode 100644 java/java-tests/testData/codeInsight/typing/semicolonBeforeRightParenMoved_before.java create mode 100644 java/java-tests/testData/codeInsight/typing/semicolonBeforeRightParenNotMoved_after.java create mode 100644 java/java-tests/testData/codeInsight/typing/semicolonBeforeRightParenNotMoved_before.java diff --git a/java/java-impl/src/com/intellij/codeInsight/editorActions/JavaTypedHandler.java b/java/java-impl/src/com/intellij/codeInsight/editorActions/JavaTypedHandler.java index ed2435cc9b8b..764a5167beb2 100644 --- a/java/java-impl/src/com/intellij/codeInsight/editorActions/JavaTypedHandler.java +++ b/java/java-impl/src/com/intellij/codeInsight/editorActions/JavaTypedHandler.java @@ -19,6 +19,7 @@ import com.intellij.codeInsight.AutoPopupController; import com.intellij.codeInsight.CodeInsightSettings; import com.intellij.codeInsight.completion.JavaClassReferenceCompletionContributor; import com.intellij.codeInsight.editorActions.smartEnter.JavaSmartEnterProcessor; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; @@ -37,6 +38,7 @@ import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.TokenSet; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.PsiUtil; +import com.intellij.util.PsiErrorElementUtil; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -108,7 +110,7 @@ public class JavaTypedHandler extends TypedHandlerDelegate { } if (c == ';') { - if (handleSemicolon(editor, fileType)) return Result.STOP; + if (handleSemicolon(editor, file, fileType)) return Result.STOP; } if (fileType == StdFileTypes.JAVA && c == '{') { int offset = editor.getCaretModel().getOffset(); @@ -245,11 +247,13 @@ public class JavaTypedHandler extends TypedHandlerDelegate { return false; } - private static boolean handleSemicolon(Editor editor, FileType fileType) { + private static boolean handleSemicolon(@NotNull Editor editor, @NotNull PsiFile file, @NotNull FileType fileType) { if (fileType != StdFileTypes.JAVA) return false; int offset = editor.getCaretModel().getOffset(); if (offset == editor.getDocument().getTextLength()) return false; + if (moveSemicolonAtRParen(editor, file, offset)) return true; + char charAt = editor.getDocument().getCharsSequence().charAt(offset); if (charAt != ';') return false; @@ -260,6 +264,87 @@ public class JavaTypedHandler extends TypedHandlerDelegate { return true; } + private static boolean moveSemicolonAtRParen(@NotNull Editor editor, @NotNull PsiFile file, int caretOffset) { + ApplicationManager.getApplication().assertWriteAccessAllowed(); + + // Disable feature if code model is not synchronized with the document. + // Note: this feature can be implemented using only lexer because there + // are not so many statements which allow semicolon inside them before + // closing paren. Therefore, in other cases, if user types semicolon + // before rparen, that usually means that he wants to end the statement. + if (PsiDocumentManager.getInstance(file.getProject()).isUncommited(editor.getDocument())) { + // But enable in unit tests to avoid commiting document after each typing + if (!ApplicationManager.getApplication().isUnitTestMode()) { + return false; + } + } + + HighlighterIterator it = ((EditorEx)editor).getHighlighter().createIterator(caretOffset); + int afterLastParenOffset = -1; + + try { + do { + if (isAtLineEnd(it)) { + break; + } + else if (it.getTokenType() == JavaTokenType.RBRACE) { + break; + } + else if (it.getTokenType() == JavaTokenType.RPARENTH) { + afterLastParenOffset = it.getEnd(); + } + else if (it.getTokenType() != TokenType.WHITE_SPACE) { + // Other tokens are not permitted + return false; + } + it.advance(); + } + while (!it.atEnd()); + } + catch (IndexOutOfBoundsException ex) { + // May be thrown when checking character at the current offset. + return false; + } + + if (!it.atEnd() && afterLastParenOffset >= 0 && afterLastParenOffset >= caretOffset) { + PsiElement curElement = file.findElementAt(caretOffset); + PsiStatement curStmt = PsiTreeUtil.getParentOfType(curElement, PsiStatement.class); + if (curStmt != null) { + if (curStmt instanceof PsiTryStatement) { + // try-with-resources can contain semicolons inside + return false; + } + if (curStmt instanceof PsiForStatement) { + // for loop can have semicolons inside + return false; + } + // It may worth to check if the error element is about expecting semicolon + if (PsiTreeUtil.getDeepestLast(curStmt) instanceof PsiErrorElement) { + int stmtEndOffset = curStmt.getTextRange().getEndOffset(); + if (stmtEndOffset == afterLastParenOffset || stmtEndOffset == it.getStart()) { + editor.getDocument().insertString(stmtEndOffset, ";"); + EditorModificationUtil.moveCaretRelatively(editor, stmtEndOffset - caretOffset + 1); + return true; + } + } + } + } + + return false; + } + + private static boolean isAtLineEnd(HighlighterIterator it) { + if (it.getTokenType() == TokenType.WHITE_SPACE) { + for (int offset = it.getStart(); offset < it.getEnd(); ++offset) { + char chr = it.getDocument().getCharsSequence().charAt(offset); + if (chr == '\n') { + return true; + } + } + } + return false; + } + private static void autoPopupJavadocLookup(final Project project, final Editor editor) { AutoPopupController.getInstance(project).autoPopupMemberLookup(editor, file -> { int offset = editor.getCaretModel().getOffset(); diff --git a/java/java-tests/testData/codeInsight/typing/semicolonBeforeRightParenMoved_after.java b/java/java-tests/testData/codeInsight/typing/semicolonBeforeRightParenMoved_after.java new file mode 100644 index 000000000000..3d8bd3be2b7e --- /dev/null +++ b/java/java-tests/testData/codeInsight/typing/semicolonBeforeRightParenMoved_after.java @@ -0,0 +1,30 @@ +class Foo { + + interface MyPredicate { + boolean apply(int param); + } + + int foo(int param1, int param2) { + return 0; + } + + int boo(Foo that) { + return 0; + } + + void zoo(MyPredicate predicate) { + } + + int test() { + foo(5, 7/*typehere*/); + foo(1, foo(2, 3/*typehere*/)); + int x = foo(3, 4/*typehere*/); + int y = (foo(3, 4/*typehere*/)); + int z = (foo(3, 4) + boo(this/*typehere*/)); + zoo(val->true/*typehere*/); + zoo(val->{ return (true/*typehere*/); }) + zoo(val->{ return (true) }/*typehere*/); + test(/*typehere*/); + return 0; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/typing/semicolonBeforeRightParenMoved_before.java b/java/java-tests/testData/codeInsight/typing/semicolonBeforeRightParenMoved_before.java new file mode 100644 index 000000000000..d2b8dd205f03 --- /dev/null +++ b/java/java-tests/testData/codeInsight/typing/semicolonBeforeRightParenMoved_before.java @@ -0,0 +1,30 @@ +class Foo { + + interface MyPredicate { + boolean apply(int param); + } + + int foo(int param1, int param2) { + return 0; + } + + int boo(Foo that) { + return 0; + } + + void zoo(MyPredicate predicate) { + } + + int test() { + foo(5, 7/*typehere*/) + foo(1, foo(2, 3/*typehere*/)) + int x = foo(3, 4/*typehere*/) + int y = (foo(3, 4/*typehere*/)) + int z = (foo(3, 4) + boo(this/*typehere*/)) + zoo(val->true/*typehere*/) + zoo(val->{ return (true/*typehere*/) }) + zoo(val->{ return (true) }/*typehere*/) + test(/*typehere*/) + return 0; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/typing/semicolonBeforeRightParenNotMoved_after.java b/java/java-tests/testData/codeInsight/typing/semicolonBeforeRightParenNotMoved_after.java new file mode 100644 index 000000000000..12e6b8101676 --- /dev/null +++ b/java/java-tests/testData/codeInsight/typing/semicolonBeforeRightParenNotMoved_after.java @@ -0,0 +1,56 @@ +class Foo { + + interface MyPredicate { + boolean apply(int param); + } + + int foo(int param1, int param2) { + return 0; + } + + int boo(Foo that) { + return 0; + } + + void zoo(MyPredicate predicate) { + } + + int testOnFinishedStatements() { + foo(5, 7/*typehere*/;); + foo(1, foo(2, 3/*typehere*/;)); + int x = foo(3, 4/*typehere*/;); + int y = (foo(3, 4/*typehere*/;)); + int z = (foo(3, 4) + boo(this/*typehere*/;)); + zoo(val->true/*typehere*/;); + zoo(val->{ return (true/*typehere*/;); }); + zoo(val->{ return (true); }/*typehere*/;); + testOnFinishedStatements(/*typehere*/;); + return 0; + } + + int testOnUnfinishedStatements() { + foo(5/*typehere*/;, 7) + foo(1, /*typehere*/;foo(2, 3)) + int x = foo(/*typehere*/;3, 4) + int y = /*typehere*/;(foo(3, 4)) + int z = (foo(3, 4) + boo(/*typehere*/;this)); + zoo(val->/*typehere*/;true) + zoo(val->{ + int x = 1; + foo(x, x/*typehere*/;); + }) + return 0; + } + + void testWithFirstLoopSemicolon() { + for (int x = 0/*typehere*/;) + } + + void testWithSecondLoopSemicolon() { + for (int x = 0; x <= 10/*typehere*/;) + } + + void testWithTryWithResources() { + try (int x = 1/*typehere*/;) + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/typing/semicolonBeforeRightParenNotMoved_before.java b/java/java-tests/testData/codeInsight/typing/semicolonBeforeRightParenNotMoved_before.java new file mode 100644 index 000000000000..627adb82a3bc --- /dev/null +++ b/java/java-tests/testData/codeInsight/typing/semicolonBeforeRightParenNotMoved_before.java @@ -0,0 +1,56 @@ +class Foo { + + interface MyPredicate { + boolean apply(int param); + } + + int foo(int param1, int param2) { + return 0; + } + + int boo(Foo that) { + return 0; + } + + void zoo(MyPredicate predicate) { + } + + int testOnFinishedStatements() { + foo(5, 7/*typehere*/); + foo(1, foo(2, 3/*typehere*/)); + int x = foo(3, 4/*typehere*/); + int y = (foo(3, 4/*typehere*/)); + int z = (foo(3, 4) + boo(this/*typehere*/)); + zoo(val->true/*typehere*/); + zoo(val->{ return (true/*typehere*/); }); + zoo(val->{ return (true); }/*typehere*/); + testOnFinishedStatements(/*typehere*/); + return 0; + } + + int testOnUnfinishedStatements() { + foo(5/*typehere*/, 7) + foo(1, /*typehere*/foo(2, 3)) + int x = foo(/*typehere*/3, 4) + int y = /*typehere*/(foo(3, 4)) + int z = (foo(3, 4) + boo(/*typehere*/this)); + zoo(val->/*typehere*/true) + zoo(val->{ + int x = 1; + foo(x, x/*typehere*/); + }) + return 0; + } + + void testWithFirstLoopSemicolon() { + for (int x = 0/*typehere*/) + } + + void testWithSecondLoopSemicolon() { + for (int x = 0; x <= 10/*typehere*/) + } + + void testWithTryWithResources() { + try (int x = 1/*typehere*/) + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/JavaTypingTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/JavaTypingTest.java index 87834a01728d..131d4f73b85a 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/JavaTypingTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/JavaTypingTest.java @@ -16,11 +16,25 @@ package com.intellij.java.codeInsight; import com.intellij.openapi.editor.ex.EditorEx; +import com.intellij.openapi.fileEditor.impl.LoadTextUtil; +import com.intellij.openapi.util.text.LineTokenizer; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDocumentManager; import com.intellij.testFramework.PlatformTestUtil; +import com.intellij.testFramework.UsefulTestCase; import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase; +import gnu.trove.THashMap; +import org.jetbrains.annotations.NotNull; import java.io.File; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class JavaTypingTest extends LightPlatformCodeInsightFixtureTestCase { public void testMulticaretIndentLBrace() { @@ -110,6 +124,14 @@ public class JavaTypingTest extends LightPlatformCodeInsightFixtureTestCase { doTest(';'); } + public void testSemicolonBeforeRightParenMoved() { + doMultiTypeTest(';'); + } + + public void testSemicolonBeforeRightParenNotMoved() { + doMultiTypeTest(';'); + } + public void testCommaAfterDefaultAnnotationArgumentWhenArrayIsExpected() { doTest(','); } @@ -122,8 +144,34 @@ public class JavaTypingTest extends LightPlatformCodeInsightFixtureTestCase { myFixture.checkResultByFile(getTestName(true) + "_after.java"); } + private void doMultiTypeTest(char c) { + myFixture.configureByFile(getTestName(true) + "_before.java"); + List whereToType = findWhereToType(myFixture.getFile().getVirtualFile()); + assertNotNull("Test file must have at least one place where to type!", whereToType); + assertNotEmpty(whereToType); + for (Integer offset : whereToType) { + myFixture.getEditor().getCaretModel().moveToOffset(offset); + myFixture.type(c); + } + myFixture.checkResultByFile(getTestName(true) + "_after.java"); + } + @Override protected String getTestDataPath() { return PlatformTestUtil.getCommunityPath().replace(File.separatorChar, '/') + "/java/java-tests/testData/codeInsight/typing"; } + + private static List findWhereToType(@NotNull VirtualFile file) { + if (file.isDirectory() || file.getFileType().isBinary() || !file.getExtension().contentEquals("java")) { + return Collections.emptyList(); + } + CharSequence text = LoadTextUtil.loadText(file); //hide CRLF/LF conversion problem + List offsets = new ArrayList<>(); + Matcher m = Pattern.compile("/\\*typehere\\*/").matcher(text); + while (m.find()) { + offsets.add(m.end()); + } + Collections.sort(offsets, (a, b) -> b - a); // sort in descending order + return offsets; + } }