diff --git a/java/java-impl/src/META-INF/JavaPlugin.xml b/java/java-impl/src/META-INF/JavaPlugin.xml index 9fd3266ab875..cb3440f0a481 100644 --- a/java/java-impl/src/META-INF/JavaPlugin.xml +++ b/java/java-impl/src/META-INF/JavaPlugin.xml @@ -1120,6 +1120,7 @@ + diff --git a/java/java-impl/src/com/intellij/codeInsight/editorActions/JavaEnterInTextBlockHandler.java b/java/java-impl/src/com/intellij/codeInsight/editorActions/JavaEnterInTextBlockHandler.java new file mode 100644 index 000000000000..e4a3087438de --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/editorActions/JavaEnterInTextBlockHandler.java @@ -0,0 +1,91 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.codeInsight.editorActions; + +import com.intellij.codeInsight.editorActions.enter.EnterInStringLiteralHandler; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.LogicalPosition; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.*; +import com.intellij.psi.codeStyle.CodeStyleManager; +import com.intellij.psi.impl.source.tree.java.PsiLiteralExpressionImpl; +import com.intellij.util.ObjectUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class JavaEnterInTextBlockHandler extends EnterInStringLiteralHandler { + + @Override + public Result postProcessEnter(@NotNull PsiFile file, @NotNull Editor editor, @NotNull DataContext dataContext) { + PsiDocumentManager.getInstance(file.getProject()).commitDocument(editor.getDocument()); + PsiLiteralExpressionImpl textBlock = getTextBlock(file, editor.getCaretModel().getOffset()); + if (textBlock == null) return super.postProcessEnter(file, editor, dataContext); + int offset = editor.getCaretModel().getOffset(); + Document document = editor.getDocument(); + if (isContentAtTheEnd(textBlock)) { + CodeStyleManager.getInstance(textBlock.getProject()).reformat(textBlock); + offset = editor.getCaretModel().getOffset(); + } + else if (!isAtBlockStart(document, textBlock, offset)) { + return super.postProcessEnter(file, editor, dataContext); + } + editor.getCaretModel().moveToOffset(findOffset(editor, document, textBlock, offset)); + return Result.Continue; + } + + private static boolean isContentAtTheEnd(@NotNull PsiLiteralExpressionImpl textBlock) { + String text = textBlock.getTextBlockText(); + if (text == null) return false; + boolean foundContent = false; + for (int i = 0; i < text.length(); i++) { + if (!isWhitespaceOrNewLine(text.charAt(i))) { + foundContent = true; + } + else if (foundContent) { + return false; + } + } + return true; + } + + private static boolean isAtBlockStart(@NotNull Document document, @NotNull PsiLiteralExpressionImpl textBlock, int offset) { + String text = document.getText(new TextRange(textBlock.getTextOffset(), offset)); + if (!text.startsWith("\"\"\"")) return false; + text = text.substring(3); + return text.chars().allMatch(JavaEnterInTextBlockHandler::isWhitespaceOrNewLine); + } + + private static boolean isWhitespaceOrNewLine(int c) { + return Character.isWhitespace(c) || c == '\n'; + } + + private static int findOffset(@NotNull Editor editor, + @NotNull Document document, + @NotNull PsiLiteralExpressionImpl textBlock, + int offset) { + TextRange afterOffset = new TextRange(offset, textBlock.getTextRange().getEndOffset()); + String text = document.getText(afterOffset); + int line = document.getLineNumber(offset); + int lineStart = document.getLineStartOffset(line); + int start = text.indexOf('\n') + 1; + for (int i = start; i < text.length(); i++) { + char c = text.charAt(i); + if (isWhitespaceOrNewLine(c)) continue; + int column = editor.offsetToLogicalPosition(offset + i).column; + String indent = StringUtil.repeatSymbol(' ', column); + document.replaceString(lineStart, offset, indent); + return editor.logicalPositionToOffset(new LogicalPosition(line, column)); + } + return offset; + } + + @Nullable + private static PsiLiteralExpressionImpl getTextBlock(@NotNull PsiFile file, int offset) { + if (!(file instanceof PsiJavaFile)) return null; + PsiJavaToken token = ObjectUtils.tryCast(file.findElementAt(offset), PsiJavaToken.class); + if (token == null || token.getTokenType() != JavaTokenType.TEXT_BLOCK_LITERAL) return null; + return ObjectUtils.tryCast(token.getParent(), PsiLiteralExpressionImpl.class); + } +} diff --git a/java/java-impl/src/com/intellij/codeInsight/editorActions/JavaQuoteHandler.java b/java/java-impl/src/com/intellij/codeInsight/editorActions/JavaQuoteHandler.java index eaf2cc2d9d28..70e6489a9193 100644 --- a/java/java-impl/src/com/intellij/codeInsight/editorActions/JavaQuoteHandler.java +++ b/java/java-impl/src/com/intellij/codeInsight/editorActions/JavaQuoteHandler.java @@ -7,8 +7,10 @@ import com.intellij.openapi.editor.highlighter.HighlighterIterator; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; import com.intellij.psi.impl.source.tree.ElementType; +import com.intellij.psi.impl.source.tree.java.PsiLiteralExpressionImpl; import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.TokenSet; +import com.intellij.util.ObjectUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -99,22 +101,37 @@ public class JavaQuoteHandler extends SimpleTokenSetQuoteHandler implements Java public boolean hasNonClosedLiteral(Editor editor, HighlighterIterator iterator, int offset) { if (iterator.getTokenType() == JavaTokenType.TEXT_BLOCK_LITERAL) { Document document = iterator.getDocument(); - if (document != null && StringUtil.equals(document.getCharsSequence().subSequence(iterator.getStart(), offset + 1), "\"\"\"")) { - return true; + if (document != null) { + String text = document.getText(); + boolean hasOpenQuotes = StringUtil.equals(text.substring(iterator.getStart(), offset + 1), "\"\"\""); + if (hasOpenQuotes) { + boolean hasCloseQuotes = StringUtil.contains(text.substring(offset + 1, iterator.getEnd()), "\"\"\""); + if (!hasCloseQuotes) return true; + // check if parser interpreted next text block start quotes as end quotes for the current one + int nTextBlockQuotes = StringUtil.getOccurrenceCount(text.substring(iterator.getEnd()), "\"\"\""); + return nTextBlockQuotes % 2 != 0; + } } } return super.hasNonClosedLiteral(editor, iterator, offset); } @Override - public void insertClosingQuote(@NotNull Editor editor, int offset, @NotNull CharSequence closingQuote) { - if (closingQuote.charAt(0) == '`') { - editor.getDocument().insertString(offset, " " + closingQuote); - editor.getSelectionModel().setSelection(offset, offset + 1); - } - else { - editor.getDocument().insertString(offset, "\n" + closingQuote); - editor.getCaretModel().moveToOffset(offset + 1); - } + public boolean needSemicolonAfter(@NotNull PsiElement element) { + PsiLiteralExpressionImpl literal = ObjectUtils.tryCast(element.getParent(), PsiLiteralExpressionImpl.class); + if (literal == null || literal.getLiteralElementType() != JavaTokenType.TEXT_BLOCK_LITERAL) return false; + PsiJavaToken token = ObjectUtils.tryCast(literal.getNextSibling(), PsiJavaToken.class); + if (token != null && token.getTokenType() == JavaTokenType.SEMICOLON) return false; + String text = literal.getText(); + if (text.indexOf('\n') != -1) return false; + PsiVariable variable = ObjectUtils.tryCast(literal.getParent(), PsiVariable.class); + if (variable != null && variable.getInitializer() == literal) return true; + PsiAssignmentExpression assignment = ObjectUtils.tryCast(literal.getParent(), PsiAssignmentExpression.class); + return assignment != null && assignment.getRExpression() == literal; + } + + @Override + public void insertClosingQuote(@NotNull Editor editor, int offset, @NotNull PsiFile file, @NotNull CharSequence closingQuote) { + editor.getDocument().insertString(offset, "\"\"\""); } } \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/editorActions/stringLiteral/contentAtTheEndOfTextBlock.java b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/contentAtTheEndOfTextBlock.java new file mode 100644 index 000000000000..79297f37439f --- /dev/null +++ b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/contentAtTheEndOfTextBlock.java @@ -0,0 +1,8 @@ +class Test { + + void test() { + String block = """ + text"""; + } + +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/editorActions/stringLiteral/contentAtTheEndOfTextBlock_after.java b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/contentAtTheEndOfTextBlock_after.java new file mode 100644 index 000000000000..b9557ce80290 --- /dev/null +++ b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/contentAtTheEndOfTextBlock_after.java @@ -0,0 +1,9 @@ +class Test { + + void test() { + String block = """ + + text"""; + } + +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/editorActions/stringLiteral/contentAtTheStartOfTextBlock.java b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/contentAtTheStartOfTextBlock.java new file mode 100644 index 000000000000..f467a635eaf3 --- /dev/null +++ b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/contentAtTheStartOfTextBlock.java @@ -0,0 +1,7 @@ +class Test { + + void test() { + String block = """text + """; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/editorActions/stringLiteral/contentAtTheStartOfTextBlock_after.java b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/contentAtTheStartOfTextBlock_after.java new file mode 100644 index 000000000000..a101e60a306f --- /dev/null +++ b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/contentAtTheStartOfTextBlock_after.java @@ -0,0 +1,8 @@ +class Test { + + void test() { + String block = """ + text + """; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/editorActions/stringLiteral/emptyTextBlock.java b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/emptyTextBlock.java new file mode 100644 index 000000000000..a1f0ce9e5af8 --- /dev/null +++ b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/emptyTextBlock.java @@ -0,0 +1,7 @@ +class Test { + + void test() { + String block = """"""; + } + +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/editorActions/stringLiteral/emptyTextBlock_after.java b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/emptyTextBlock_after.java new file mode 100644 index 000000000000..71de243683ef --- /dev/null +++ b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/emptyTextBlock_after.java @@ -0,0 +1,8 @@ +class Test { + + void test() { + String block = """ + """; + } + +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/editorActions/stringLiteral/noSemicolon.java b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/noSemicolon.java new file mode 100644 index 000000000000..26f901704c75 --- /dev/null +++ b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/noSemicolon.java @@ -0,0 +1,7 @@ +class Test { + + void test() { + String block = """""" + } + +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/editorActions/stringLiteral/noSemicolon_after.java b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/noSemicolon_after.java new file mode 100644 index 000000000000..feea8398ec71 --- /dev/null +++ b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/noSemicolon_after.java @@ -0,0 +1,8 @@ +class Test { + + void test() { + String block = """ + """; + } + +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/editorActions/stringLiteral/nonIndentedTextBlockContent.java b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/nonIndentedTextBlockContent.java new file mode 100644 index 000000000000..c6ba9dee26bd --- /dev/null +++ b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/nonIndentedTextBlockContent.java @@ -0,0 +1,9 @@ +class Test { + + void foo() { + String block = """ +bar +foo +"""; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/editorActions/stringLiteral/nonIndentedTextBlockContent_after.java b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/nonIndentedTextBlockContent_after.java new file mode 100644 index 000000000000..0897b79d4d2a --- /dev/null +++ b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/nonIndentedTextBlockContent_after.java @@ -0,0 +1,10 @@ +class Test { + + void foo() { + String block = """ +bar + +foo +"""; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/editorActions/stringLiteral/onlyWhitespacesTextBlock.java b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/onlyWhitespacesTextBlock.java new file mode 100644 index 000000000000..3d412d92b971 --- /dev/null +++ b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/onlyWhitespacesTextBlock.java @@ -0,0 +1,9 @@ +class Test { + void test() { + String block = """ + + + + """; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/editorActions/stringLiteral/onlyWhitespacesTextBlock_after.java b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/onlyWhitespacesTextBlock_after.java new file mode 100644 index 000000000000..6dea07989a05 --- /dev/null +++ b/java/java-tests/testData/codeInsight/editorActions/stringLiteral/onlyWhitespacesTextBlock_after.java @@ -0,0 +1,10 @@ +class Test { + void test() { + String block = """ + + + + + """; + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/editorActions/JavaEnterInStringLiteralTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/editorActions/JavaEnterInStringLiteralTest.java index 9d5a7d23a74c..f80747c31767 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/editorActions/JavaEnterInStringLiteralTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/editorActions/JavaEnterInStringLiteralTest.java @@ -27,6 +27,30 @@ import com.intellij.testFramework.LightJavaCodeInsightTestCase; public class JavaEnterInStringLiteralTest extends LightJavaCodeInsightTestCase { private static final String BASE_PATH = "/codeInsight/editorActions/stringLiteral/"; + public void testNonIndentedTextBlockContent() { + doTest(); + } + + public void testContentAtTheStartOfTextBlock() { + doTest(); + } + + public void testOnlyWhitespacesTextBlock() { + doTest(); + } + + public void testContentAtTheEndOfTextBlock() { + doTest(); + } + + public void testEmptyTextBlock() { + doTest(); + } + + public void testNoSemicolon() { + doTest(); + } + public void testEnter() { doTest(); } diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/editorActions/JavaQuoteTest.groovy b/java/java-tests/testSrc/com/intellij/java/codeInsight/editorActions/JavaQuoteTest.groovy index aa34fb38996e..3e46d8ab1fc1 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/editorActions/JavaQuoteTest.groovy +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/editorActions/JavaQuoteTest.groovy @@ -23,7 +23,7 @@ class JavaQuoteTest extends LightJavaCodeInsightFixtureTestCase { void testSingleInComment() { doTest '/* */', '/* \' */', "'" as char } void testSingleInStringAfterEscape() { doTest ''' split(text, '\\); ''', ''' split(text, '\\'); ''', "'" as char } - void testTextBlock() { doTest ' "" ', ' """\n""" ' } + void testTextBlock() { doTest ' "" ', ' """""" ' } void testDoubleQuoteInTextBlock() { doTest ' """ """ ', ' """ " """ ' } void testSingleQuoteInTextBlock() { doTest ' """ """ ', ' """ \' """ ', "'" as char } void testTextBlockClosing() { @@ -31,7 +31,7 @@ class JavaQuoteTest extends LightJavaCodeInsightFixtureTestCase { doTest ' """.""" ', ' """.""" ' doTest ' """.""" ', ' """.""" ' } - void testPrecedingTextBlock() { doTest 'f("" + """\n .""")', 'f("""\n""" + """\n .""")' } + void testPrecedingTextBlock() { doTest 'f("" + """\n .""")', 'f("""""" + """\n .""")' } private void doTest(String before, String after, char c = '"') { myFixture.configureByText("a.java", "class C {{\n ${before}\n}}") diff --git a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/JavaLikeQuoteHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/JavaLikeQuoteHandler.java index a59ae0d8bcc1..5e9b7057ce51 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/JavaLikeQuoteHandler.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/JavaLikeQuoteHandler.java @@ -23,4 +23,8 @@ public interface JavaLikeQuoteHandler extends QuoteHandler { boolean isAppropriateElementTypeForLiteral(@NotNull IElementType tokenType); boolean needParenthesesAroundConcatenation(PsiElement element); + + default boolean needSemicolonAfter(@NotNull PsiElement element) { + return false; + } } \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/enter/EnterInStringLiteralHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/enter/EnterInStringLiteralHandler.java index 3af30100e0cc..c24043d2d7a2 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/enter/EnterInStringLiteralHandler.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/enter/EnterInStringLiteralHandler.java @@ -45,9 +45,9 @@ public class EnterInStringLiteralHandler extends EnterHandlerDelegateAdapter { PsiDocumentManager.getInstance(file.getProject()).commitDocument(editor.getDocument()); PsiElement psiAtOffset = file.findElementAt(caretOffset); if (psiAtOffset != null && psiAtOffset.getTextOffset() < caretOffset) { + Document document = editor.getDocument(); if (quoteHandler.canBeConcatenated(psiAtOffset)) { ASTNode token = psiAtOffset.getNode(); - Document document = editor.getDocument(); CharSequence text = document.getText(); TextRange range = token.getTextRange(); @@ -83,6 +83,9 @@ public class EnterInStringLiteralHandler extends EnterHandlerDelegateAdapter { caretAdvanceRef.set(caretAdvance); return Result.DefaultForceIndent; } + if (quoteHandler.needSemicolonAfter(psiAtOffset)) { + document.insertString(psiAtOffset.getTextRange().getEndOffset(), ";"); + } } return Result.Continue; }