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;
}