Implemented IDEABKL-4743 Automatic placement of

This commit is contained in:
Petr Kudriavtsev
2018-01-12 17:35:01 +03:00
parent 9e17a01768
commit 0a419eb1c0
6 changed files with 307 additions and 2 deletions
@@ -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();
@@ -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;
}
}
@@ -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;
}
}
@@ -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*/;)
}
}
@@ -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*/)
}
}
@@ -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<Integer> 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<Integer> 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<Integer> 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;
}
}