This is an automated email from the ASF dual-hosted git repository. jlahoda pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/netbeans.git
The following commit(s) were added to refs/heads/master by this push: new cb50c2f JDK13 text block support cb50c2f is described below commit cb50c2fede0387535900727e1abdcd4718ab02fd Author: Reema Taneja <32299405+rtane...@users.noreply.github.com> AuthorDate: Sun Aug 25 14:14:54 2019 +0530 JDK13 text block support --- ide/editor.indent/apichanges.xml | 12 ++ .../modules/editor/indent/spi/Context.java | 32 ++++- .../src/org/netbeans/editor/BaseKit.java | 51 +++++-- .../editor/base/semantic/ColoringAttributes.java | 4 +- .../base/semantic/SemanticHighlighterBase.java | 59 +++++++- .../java/editor/base/semantic/TokenList.java | 24 ++++ .../java/editor/base/semantic/DetectorTest.java | 43 ++++++ .../java/editor/base/semantic/HighlightImpl.java | 1 + .../java/editor/base/semantic/MarkOccDetTest.java | 17 ++- .../java/editor/base/semantic/TestBase.java | 80 ++++++++++- .../modules/editor/java/TypingCompletion.java | 53 +++++-- .../java/editor/imports/ClipboardHandler.java | 128 ++++++++++++----- .../modules/java/editor/resources/fontsColors.xml | 1 + .../java/editor/semantic/ColoringManager.java | 1 + .../java/editor/semantic/SemanticHighlighter.java | 8 +- .../editor/java/TypingCompletionUnitTest.java | 63 ++++++++- .../java/editor/imports/ClipboardHandlerTest.java | 15 ++ .../modules/java/hints/jdk/ConvertToTextBlock.java | 109 +++++++++++++++ .../modules/java/hints/resources/jdk13.properties | 17 +++ .../modules/java/hints/resources/layer.xml | 16 +++ .../java/hints/jdk/ConvertToTextBlockTest.java | 155 +++++++++++++++++++++ java/java.lexer/apichanges.xml | 12 ++ .../org/netbeans/api/java/lexer/JavaTokenId.java | 2 + .../src/org/netbeans/lib/java/lexer/JavaLexer.java | 24 +++- .../lib/java/lexer/JavaLexerBatchTest.java | 35 ++++- .../org/netbeans/api/java/source/SourceUtils.java | 26 +++- .../modules/java/source/builder/TreeFactory.java | 2 + .../modules/java/source/pretty/CharBuffer.java | 16 ++- .../modules/java/source/pretty/VeryPretty.java | 34 ++++- .../modules/java/source/save/Reindenter.java | 34 ++++- .../netbeans/api/java/source/gen/LiteralTest.java | 88 ++++++++++++ .../modules/java/source/save/ReindenterTest.java | 35 ++++- 32 files changed, 1104 insertions(+), 93 deletions(-) diff --git a/ide/editor.indent/apichanges.xml b/ide/editor.indent/apichanges.xml index 932020a..255737d 100644 --- a/ide/editor.indent/apichanges.xml +++ b/ide/editor.indent/apichanges.xml @@ -83,6 +83,18 @@ is the proper place. <!-- ACTUAL CHANGES BEGIN HERE: --> <changes> + <change id="indent.given.indent"> + <summary>Indent can be given as a string</summary> + <version major="1" minor="49"/> + <date day="16" month="6" year="2019"/> + <author login="jlahoda"/> + <compatibility source="compatible" binary="compatible"/> + <description> + Added method org.netbeans.modules.editor.indent.spi.Context.modifyIndent(int, int, String), + which allows to set indent by specifying particular indent string. + </description> + <class package="org.netbeans.modules.editor.indent.spi" name="Context"/> + </change> <change id="indent.support"> <summary>Indentation Support module created</summary> <version major="1" minor="39"/> diff --git a/ide/editor.indent/src/org/netbeans/modules/editor/indent/spi/Context.java b/ide/editor.indent/src/org/netbeans/modules/editor/indent/spi/Context.java index a9d1108..0ed70ad 100644 --- a/ide/editor.indent/src/org/netbeans/modules/editor/indent/spi/Context.java +++ b/ide/editor.indent/src/org/netbeans/modules/editor/indent/spi/Context.java @@ -170,23 +170,43 @@ public final class Context { } String newIndentString = IndentUtils.createIndentString(doc, newIndent); + + modifyIndent(lineStartOffset, oldIndentEndOffset - lineStartOffset, newIndentString); + } + + /** + * Modify indent of the line at the offset passed as the parameter, by stripping the given + * number of input characters and inserting the given indent. + * + * @param lineStartOffset start offset of a line where the indent is being modified. + * @param oldIndentCharCount number of characters to remove. + * @param newIndent new indent. + * @throws javax.swing.text.BadLocationException if the given lineStartOffset is not within + * corresponding document's bounds. + * @since 1.49 + */ + public void modifyIndent(int lineStartOffset, int oldIndentCharCount, String newIndent) throws BadLocationException { + Document doc = document(); + IndentImpl.checkOffsetInDocument(doc, lineStartOffset); + CharSequence docText = DocumentUtilities.getText(doc); + int oldIndentEndOffset = lineStartOffset + oldIndentCharCount; // Attempt to match the begining characters int offset = lineStartOffset; - for (int i = 0; i < newIndentString.length() && lineStartOffset + i < oldIndentEndOffset; i++) { - if (newIndentString.charAt(i) != docText.charAt(lineStartOffset + i)) { + for (int i = 0; i < newIndent.length() && lineStartOffset + i < oldIndentEndOffset; i++) { + if (newIndent.charAt(i) != docText.charAt(lineStartOffset + i)) { offset = lineStartOffset + i; - newIndentString = newIndentString.substring(i); + newIndent = newIndent.substring(i); break; } } // Replace the old indent - if (!doc.getText(offset, oldIndentEndOffset - offset).equals(newIndentString)) { + if (!doc.getText(offset, oldIndentEndOffset - offset).equals(newIndent)) { if (offset < oldIndentEndOffset) { doc.remove(offset, oldIndentEndOffset - offset); } - if (newIndentString.length() > 0) { - doc.insertString(offset, newIndentString, null); + if (newIndent.length() > 0) { + doc.insertString(offset, newIndent, null); } } } diff --git a/ide/editor.lib/src/org/netbeans/editor/BaseKit.java b/ide/editor.lib/src/org/netbeans/editor/BaseKit.java index b36c6c9..0add683 100644 --- a/ide/editor.lib/src/org/netbeans/editor/BaseKit.java +++ b/ide/editor.lib/src/org/netbeans/editor/BaseKit.java @@ -1374,25 +1374,48 @@ public class BaseKit extends DefaultEditorKit { editorUI.getWordMatch().clear(); // reset word matching Boolean overwriteMode = (Boolean)editorUI.getProperty(EditorUI.OVERWRITE_MODE_PROPERTY); boolean ovr = (overwriteMode != null && overwriteMode.booleanValue()); - if (Utilities.isSelectionShowing(caret)) { // valid selection - try { - doc.putProperty(DOC_REPLACE_SELECTION_PROPERTY, true); - replaceSelection(target, insertionOffset, caret, insertionText, ovr); - } finally { - doc.putProperty(DOC_REPLACE_SELECTION_PROPERTY, null); + int currentInsertOffset = insertionOffset; + int targetCaretOffset = caretPosition; + for (int i = 0; i < insertionText.length();) { + int end = insertionText.indexOf('\n', i); + if (end == (-1)) end = insertionText.length(); + String currentLine = insertionText.substring(i, end); + if (i == 0) { + if (Utilities.isSelectionShowing(caret)) { // valid selection + try { + doc.putProperty(DOC_REPLACE_SELECTION_PROPERTY, true); + replaceSelection(target, currentInsertOffset, caret, currentLine, ovr); + } finally { + doc.putProperty(DOC_REPLACE_SELECTION_PROPERTY, null); + } + } else { // no selection + if (ovr && currentInsertOffset < doc.getLength() && doc.getChars(currentInsertOffset, 1)[0] != '\n') { //NOI18N + // overwrite current char + insertString(doc, currentInsertOffset, caret, currentLine, true); + } else { // insert mode + insertString(doc, currentInsertOffset, caret, currentLine, false); + } + } + } else { + Indent indent = Indent.get(doc); + indent.lock(); + try { + currentInsertOffset = indent.indentNewLine(currentInsertOffset); + } finally { + indent.unlock(); + } + insertString(doc, currentInsertOffset, caret, currentLine, false); } - } else { // no selection - if (ovr && insertionOffset < doc.getLength() && doc.getChars(insertionOffset, 1)[0] != '\n') { //NOI18N - // overwrite current char - insertString(doc, insertionOffset, caret, insertionText, true); - } else { // insert mode - insertString(doc, insertionOffset, caret, insertionText, false); + if (caretPosition >= i && caretPosition <= end) { + targetCaretOffset = currentInsertOffset - insertionOffset + caretPosition - i; } + currentInsertOffset += currentLine.length(); + i = end + 1; } - if (caretPosition != -1) { + if (targetCaretOffset != -1) { assert caretPosition >= 0 && (caretPosition <= insertionText.length()); - caret.setDot(insertionOffset + caretPosition); + caret.setDot(insertionOffset + targetCaretOffset); } } finally { DocumentUtilities.setTypingModification(doc, false); diff --git a/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/ColoringAttributes.java b/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/ColoringAttributes.java index b9796f7..9f09f17 100644 --- a/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/ColoringAttributes.java +++ b/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/ColoringAttributes.java @@ -62,7 +62,9 @@ public enum ColoringAttributes { KEYWORD, - JAVADOC_IDENTIFIER; + JAVADOC_IDENTIFIER, + + UNINDENTED_TEXT_BLOCK; public static Coloring empty() { return new Coloring(); diff --git a/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/SemanticHighlighterBase.java b/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/SemanticHighlighterBase.java index 58c7a05..5d6d51c 100644 --- a/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/SemanticHighlighterBase.java +++ b/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/SemanticHighlighterBase.java @@ -65,12 +65,14 @@ import javax.lang.model.element.Modifier; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.swing.text.Document; +import org.netbeans.api.java.lexer.JavaTokenId; import org.netbeans.api.java.source.CompilationInfo; import org.netbeans.api.java.source.ElementHandle; import org.netbeans.api.java.source.JavaParserResultTask; import org.netbeans.api.java.source.JavaSource.Phase; import org.netbeans.api.java.source.TreeUtilities; import org.netbeans.api.java.source.support.CancellableTreePathScanner; +import org.netbeans.api.lexer.PartType; import org.netbeans.api.lexer.Token; import org.netbeans.api.lexer.TokenHierarchy; //import org.netbeans.modules.editor.NbEditorUtilities; @@ -81,6 +83,8 @@ import org.netbeans.modules.parsing.spi.Scheduler; import org.netbeans.modules.parsing.spi.SchedulerEvent; import org.netbeans.modules.parsing.spi.TaskIndexingMode; import org.openide.filesystems.FileUtil; +import org.openide.util.Exceptions; +import org.openide.util.Pair; /** @@ -249,23 +253,25 @@ public abstract class SemanticHighlighterBase extends JavaParserResultTask { boolean computeUnusedImports = "text/x-java".equals(FileUtil.getMIMEType(info.getFileObject())); - List<int[]> imports = computeUnusedImports ? new ArrayList<int[]>() : null; + List<Pair<int[], Coloring>> extraColoring = computeUnusedImports ? new ArrayList<>(v.extraColoring) : v.extraColoring; if (computeUnusedImports) { Collection<TreePath> unusedImports = UnusedImports.process(info, cancel); if (unusedImports == null) return true; + Coloring unused = collection2Coloring(Arrays.asList(ColoringAttributes.UNUSED)); + for (TreePath tree : unusedImports) { if (cancel.get()) { return true; } //XXX: finish - imports.add(new int[] { + extraColoring.add(Pair.of(new int[] { (int) info.getTrees().getSourcePositions().getStartPosition(cu, tree.getLeaf()), (int) info.getTrees().getSourcePositions().getEndPosition(cu, tree.getLeaf()) - }); + }, unused)); } } @@ -320,7 +326,7 @@ public abstract class SemanticHighlighterBase extends JavaParserResultTask { return true; if (computeUnusedImports) { - setter.setHighlights(doc, imports, v.preText); + setter.setHighlights(doc, extraColoring, v.preText); } setter.setColorings(doc, newColoring); @@ -407,6 +413,7 @@ public abstract class SemanticHighlighterBase extends JavaParserResultTask { private Map<Element, List<Use>> type2Uses; private Map<Tree, List<Token>> tree2Tokens; private List<Token> contextKeywords; + private List<Pair<int[], Coloring>> extraColoring; private Map<int[], String> preText; private TokenList tl; private long memberSelectBypass = -1; @@ -421,6 +428,7 @@ public abstract class SemanticHighlighterBase extends JavaParserResultTask { type2Uses = new HashMap<Element, List<Use>>(); tree2Tokens = new IdentityHashMap<Tree, List<Token>>(); contextKeywords = new ArrayList<>(); + extraColoring = new ArrayList<>(); preText = new HashMap<>(); tl = new TokenList(info, doc, cancel); @@ -1063,8 +1071,36 @@ public abstract class SemanticHighlighterBase extends JavaParserResultTask { return null; } + private static final Coloring UNINDENTED_TEXT_BLOCK = + ColoringAttributes.add(ColoringAttributes.empty(), ColoringAttributes.UNINDENTED_TEXT_BLOCK); + @Override public Void visitLiteral(LiteralTree node, Void p) { + int startPos = (int) info.getTrees().getSourcePositions().getStartPosition(info.getCompilationUnit(), node); + tl.moveToOffset(startPos); + Token t = tl.currentToken(); + if (t != null && t.id() == JavaTokenId.MULTILINE_STRING_LITERAL && t.partType() == PartType.COMPLETE) { + String tokenText = t.text().toString(); + String[] lines = tokenText.split("\n"); + int indent = Arrays.stream(lines, 1, lines.length) + .mapToInt(this::leadingIndent) + .min() + .orElse(0); + int pos = startPos + lines[0].length() + 1; + for (int i = 1; i < lines.length; i++) { + String line = lines[i]; + if (i == lines.length - 1) { + line = line.substring(0, line.length() - 3); + } + String strippendLine = line.replaceAll("[\t ]+$", ""); + int indentedStart = pos + indent; + int indentedEnd = pos + strippendLine.length(); + if (indentedEnd > indentedStart) + extraColoring.add(Pair.of(new int[] {indentedStart, indentedEnd}, UNINDENTED_TEXT_BLOCK)); + pos += line.length() + 1; + } + } + TreePath pp = getCurrentPath().getParentPath(); if (pp.getLeaf() != null && pp.getLeaf().getKind() == Kind.METHOD_INVOCATION) { @@ -1085,11 +1121,24 @@ public abstract class SemanticHighlighterBase extends JavaParserResultTask { return super.visitLiteral(node, p); } + private int leadingIndent(String line) { + int indent = 0; + + for (int i = 0; i < line.length(); i++) { //TODO: code points + if (Character.isWhitespace(line.charAt(i))) + indent++; + else + break; + } + + return indent; + } + } public static interface ErrorDescriptionSetter { - public void setHighlights(Document doc, Collection<int[]> highlights, Map<int[], String> preText); + public void setHighlights(Document doc, Collection<Pair<int[], Coloring>> highlights, Map<int[], String> preText); public void setColorings(Document doc, Map<Token, Coloring> colorings); } } diff --git a/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/TokenList.java b/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/TokenList.java index 1bab7c3..cadc339 100644 --- a/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/TokenList.java +++ b/java/java.editor.base/src/org/netbeans/modules/java/editor/base/semantic/TokenList.java @@ -240,6 +240,30 @@ public class TokenList { }); } + public Token currentToken() { + Token[] res = new Token[1]; + doc.render(new Runnable() { + @Override + public void run() { + if (cancel.get()) { + return ; + } + + if (ts != null && !ts.isValid()) { + cancel.set(true); + return ; + } + + if (ts == null) { + return ; + } + + res[0] = ts.token(); + } + }); + return res[0]; + } + public void moduleNameHere(final ExpressionTree tree, final Map<Tree, List<Token>> tree2Tokens) { doc.render(new Runnable() { @Override diff --git a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/DetectorTest.java b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/DetectorTest.java index 1f25ff4..4dcece3 100644 --- a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/DetectorTest.java +++ b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/DetectorTest.java @@ -21,6 +21,7 @@ package org.netbeans.modules.java.editor.base.semantic; import java.util.logging.Handler; import java.util.logging.LogRecord; import java.util.logging.Logger; +import javax.lang.model.SourceVersion; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import org.netbeans.api.java.source.CompilationController; @@ -445,6 +446,35 @@ public class DetectorTest extends TestBase { performTest("IncDecReading230408"); } + public void testRawStringLiteral() throws Exception { + try { + SourceVersion.valueOf("RELEASE_13"); + } catch (IllegalArgumentException iae) { + //OK, presumably no support for raw string literals + } + setSourceLevel("13"); + performTest("RawStringLiteral", + "public class RawStringLiteral {\n" + + " String s1 = \"\"\"\n" + + " int i1 = 1; \n" + + " int i2 = 2;\n" + + " \"\"\";\n" + + " String s2 = \"\"\"\n" + + " int i1 = 1; \n" + + " int i2 = 2;\n" + + " \"\"\";\n" + + "}\n", + "[PUBLIC, CLASS, DECLARATION], 0:13-0:29", + "[PUBLIC, CLASS], 1:4-1:10", + "[PACKAGE_PRIVATE, FIELD, DECLARATION], 1:11-1:13", + "[UNINDENTED_TEXT_BLOCK], 2:13-2:27", + "[UNINDENTED_TEXT_BLOCK], 3:13-3:29", + "[PUBLIC, CLASS], 5:4-5:10", + "[PACKAGE_PRIVATE, FIELD, DECLARATION], 5:11-5:13", + "[UNINDENTED_TEXT_BLOCK], 6:16-6:27", + "[UNINDENTED_TEXT_BLOCK], 7:16-7:29"); + } + private void performTest(String fileName) throws Exception { performTest(fileName, new Performer() { public void compute(CompilationController parameter, Document doc, final ErrorDescriptionSetter setter) { @@ -471,6 +501,19 @@ public class DetectorTest extends TestBase { }, false, expected); } + private void performTest(String fileName, String code, String... expected) throws Exception { + performTest(fileName, code, new Performer() { + public void compute(CompilationController parameter, Document doc, final ErrorDescriptionSetter setter) { + new SemanticHighlighterBase() { + @Override + protected boolean process(CompilationInfo info, Document doc) { + return process(info, doc, setter); + } + }.process(parameter, doc); + } + }, expected); + } + private FileObject testSourceFO; static { diff --git a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/HighlightImpl.java b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/HighlightImpl.java index dffd3e5..6ee1410 100644 --- a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/HighlightImpl.java +++ b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/HighlightImpl.java @@ -142,6 +142,7 @@ public final class HighlightImpl { ColoringAttributes.DECLARATION, ColoringAttributes.MARK_OCCURRENCES, + ColoringAttributes.UNINDENTED_TEXT_BLOCK, }); public static HighlightImpl parse(StyledDocument doc, String line) throws ParseException, BadLocationException { diff --git a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/MarkOccDetTest.java b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/MarkOccDetTest.java index cd07ba6..63f5b27 100644 --- a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/MarkOccDetTest.java +++ b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/MarkOccDetTest.java @@ -20,6 +20,7 @@ package org.netbeans.modules.java.editor.base.semantic; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import javax.swing.text.Document; import javax.swing.text.StyledDocument; import junit.framework.Test; @@ -27,10 +28,12 @@ import junit.framework.TestSuite; import org.netbeans.api.java.source.CompilationController; import org.netbeans.api.java.source.CompilationInfo; import org.netbeans.api.java.source.test.support.MemoryValidator; +import org.netbeans.modules.java.editor.base.semantic.ColoringAttributes.Coloring; import org.netbeans.modules.java.editor.options.MarkOccurencesSettings; import org.netbeans.modules.java.editor.base.semantic.TestBase.Performer; import org.netbeans.modules.parsing.spi.SchedulerEvent; import org.openide.text.NbDocument; +import org.openide.util.Pair; /**XXX: constructors throwing an exception are not marked as exit points * @@ -343,6 +346,9 @@ public class MarkOccDetTest extends TestBase { performTest(name, line, column, false); } + private static final Coloring MARK_OCCURRENCES = + ColoringAttributes.add(ColoringAttributes.empty(), ColoringAttributes.MARK_OCCURRENCES); + private void performTest(String name, final int line, final int column, boolean doCompileRecursively) throws Exception { performTest(name,new Performer() { public void compute(CompilationController info, Document doc, SemanticHighlighterBase.ErrorDescriptionSetter setter) { @@ -350,19 +356,18 @@ public class MarkOccDetTest extends TestBase { List<int[]> spans = new MarkOccurrencesHighlighterBase() { @Override protected void process(CompilationInfo info, Document doc, SchedulerEvent event) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + throw new UnsupportedOperationException("Not supported yet."); } }.processImpl(info, MarkOccurencesSettings.getCurrentNode(), doc, offset); if (spans != null) { - setter.setHighlights(doc, spans, Collections.<int[], String>emptyMap()); + setter.setHighlights(doc, spans.stream() + .map(span -> Pair.of(span, MARK_OCCURRENCES)) + .collect(Collectors.toList()), + Collections.<int[], String>emptyMap()); } } }, doCompileRecursively); } - protected ColoringAttributes getColoringAttribute() { - return ColoringAttributes.MARK_OCCURRENCES; - } - } diff --git a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/TestBase.java b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/TestBase.java index 7b2aca5..9d681b6 100644 --- a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/TestBase.java +++ b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/semantic/TestBase.java @@ -31,6 +31,7 @@ import java.io.StringWriter; import java.io.Writer; import java.net.URL; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.EnumSet; @@ -40,6 +41,7 @@ import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.CountDownLatch; +import java.util.stream.Collectors; import javax.swing.text.Document; import org.netbeans.api.java.lexer.JavaTokenId; import org.netbeans.api.java.source.Task; @@ -47,6 +49,7 @@ import org.netbeans.api.java.source.CompilationController; import org.netbeans.api.java.source.JavaSource; import org.netbeans.api.java.source.JavaSource.Phase; import org.netbeans.api.java.source.SourceUtilsTestUtil; +import org.netbeans.api.java.source.TestUtilities; import org.netbeans.api.java.source.TreePathHandle; import org.netbeans.api.lexer.Language; import org.netbeans.api.lexer.Token; @@ -57,6 +60,7 @@ import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.filesystems.MIMEResolver; import org.openide.loaders.DataObject; +import org.openide.util.Pair; /** * @@ -230,8 +234,74 @@ public abstract class TestBase extends NbTestCase { validator.validate(out.toString()); } - protected ColoringAttributes getColoringAttribute() { - return ColoringAttributes.UNUSED; + protected void performTest(String fileName, String code, Performer performer, String... expected) throws Exception { + performTest(fileName, code, performer, false, expected); + } + + protected void performTest(String fileName, String code, Performer performer, boolean doCompileRecursively, String... expected) throws Exception { + SourceUtilsTestUtil.prepareTest(new String[] {"org/netbeans/modules/java/editor/resources/layer.xml"}, new Object[] {new MIMEResolverImpl()}); + + FileObject scratch = SourceUtilsTestUtil.makeScratchDir(this); + FileObject cache = scratch.createFolder("cache"); + + File wd = getWorkDir(); + File testSource = new File(wd, "test/" + fileName + ".java"); + + testSource.getParentFile().mkdirs(); + TestUtilities.copyStringToFile(testSource, code); + + testSourceFO = FileUtil.toFileObject(testSource); + + assertNotNull(testSourceFO); + + if (sourceLevel != null) { + SourceUtilsTestUtil.setSourceLevel(testSourceFO, sourceLevel); + } + + File testBuildTo = new File(wd, "test-build"); + + testBuildTo.mkdirs(); + + FileObject srcRoot = FileUtil.toFileObject(testSource.getParentFile()); + SourceUtilsTestUtil.prepareTest(srcRoot,FileUtil.toFileObject(testBuildTo), cache); + + if (doCompileRecursively) { + SourceUtilsTestUtil.compileRecursively(srcRoot); + } + + final Document doc = getDocument(testSourceFO); + final List<HighlightImpl> highlights = new ArrayList<HighlightImpl>(); + + JavaSource source = JavaSource.forFileObject(testSourceFO); + + assertNotNull(source); + + final CountDownLatch l = new CountDownLatch(1); + + source.runUserActionTask(new Task<CompilationController>() { + public void run(CompilationController parameter) { + try { + parameter.toPhase(Phase.UP_TO_DATE); + + ErrorDescriptionSetterImpl setter = new ErrorDescriptionSetterImpl(); + + performer.compute(parameter, doc, setter); + + highlights.addAll(setter.highlights); + } catch (IOException e) { + e.printStackTrace(); + } finally { + l.countDown(); + } + } + }, true); + + l.await(); + + assertEquals(Arrays.asList(expected), + highlights.stream() + .map(h -> h.getHighlightTestData()) + .collect(Collectors.toList())); } public static Collection<HighlightImpl> toHighlights(Document doc, Map<Token, Coloring> colors) { @@ -290,9 +360,9 @@ public abstract class TestBase extends NbTestCase { } @Override - public void setHighlights(Document doc, Collection<int[]> highlights, Map<int[], String> preText) { - for (int[] h : highlights) { - this.highlights.add(new HighlightImpl(doc, h[0], h[1], EnumSet.of(getColoringAttribute()))); + public void setHighlights(Document doc, Collection<Pair<int[], Coloring>> highlights, Map<int[], String> preText) { + for (Pair<int[], Coloring> h : highlights) { + this.highlights.add(new HighlightImpl(doc, h.first()[0], h.first()[1], h.second())); } if (showPrependedText) { for (Entry<int[], String> e : preText.entrySet()) { diff --git a/java/java.editor/src/org/netbeans/modules/editor/java/TypingCompletion.java b/java/java.editor/src/org/netbeans/modules/editor/java/TypingCompletion.java index be2f82b..1641a25 100644 --- a/java/java.editor/src/org/netbeans/modules/editor/java/TypingCompletion.java +++ b/java/java.editor/src/org/netbeans/modules/editor/java/TypingCompletion.java @@ -22,18 +22,24 @@ import java.util.EnumSet; import java.util.List; import java.util.Set; import java.util.prefs.Preferences; +import javax.lang.model.SourceVersion; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.api.editor.settings.SimpleValueNames; import org.netbeans.api.java.lexer.JavaTokenId; +import static org.netbeans.api.java.source.SourceUtils.isTextBlockSupported; import org.netbeans.api.lexer.PartType; import org.netbeans.api.lexer.TokenHierarchy; import org.netbeans.api.lexer.TokenSequence; import org.netbeans.editor.BaseDocument; +import org.netbeans.modules.editor.NbEditorUtilities; +import org.netbeans.modules.editor.indent.api.Indent; import org.netbeans.spi.editor.typinghooks.DeletedTextInterceptor; import org.netbeans.spi.editor.typinghooks.TypedBreakInterceptor; import org.netbeans.spi.editor.typinghooks.TypedTextInterceptor; +import org.openide.filesystems.FileObject; +import org.openide.loaders.DataObject; /** * This static class groups the whole aspect of bracket completion. It is @@ -138,9 +144,16 @@ class TypingCompletion { char chr = context.getDocument().getText(context.getOffset(), 1).charAt(0); - if (chr == ')' || chr == ',' || chr == '\"' || chr == '\'' || chr == ' ' || chr == ']' || chr == '}' || chr == '\n' || chr == '\t' || chr == ';') { + if (chr == ')' || chr == ',' || chr == '\'' || chr == ' ' || chr == ']' || chr == '}' || chr == '\n' || chr == '\t' || chr == ';') { char insChr = context.getText().charAt(0); - context.setText("" + insChr + matching(insChr) , 1); // NOI18N + context.setText("" + insChr + matching(insChr), 1); // NOI18N + } else if (chr == '\"') { + if ((context.getOffset() > 2 && context.getDocument().getText(context.getOffset() - 2, 3).equals("\"\"\"")) && + isTextBlockSupported(getFileObject((BaseDocument) context.getDocument()))) { + context.setText("\"\n\"\"\"", 2); // NOI18N + } else { + context.setText("\"\"", 1); // NOI18N + } } } @@ -202,7 +215,6 @@ class TypingCompletion { TokenSequence<JavaTokenId> javaTS = javaTokenSequence(context, true); JavaTokenId id = (javaTS != null) ? javaTS.token().id() : null; - // If caret within comment return false boolean caretInsideToken = (id != null) && (javaTS.offset() + javaTS.token().length() > context.getOffset() @@ -214,7 +226,8 @@ class TypingCompletion { boolean completablePosition = isQuoteCompletablePosition(context); boolean insideString = caretInsideToken && (id == JavaTokenId.STRING_LITERAL - || id == JavaTokenId.CHAR_LITERAL); + || id == JavaTokenId.CHAR_LITERAL + || id == JavaTokenId.MULTILINE_STRING_LITERAL); int lastNonWhite = org.netbeans.editor.Utilities.getRowLastNonWhite((BaseDocument) context.getDocument(), context.getOffset()); // eol - true if the caret is at the end of line (ignoring whitespaces) @@ -231,7 +244,7 @@ class TypingCompletion { javaTS.move(context.getOffset() - 1); if (javaTS.moveNext()) { id = javaTS.token().id(); - if (id == JavaTokenId.STRING_LITERAL || id == JavaTokenId.CHAR_LITERAL) { + if (id == JavaTokenId.STRING_LITERAL || id == JavaTokenId.CHAR_LITERAL || id == JavaTokenId.MULTILINE_STRING_LITERAL) { context.setText("", 0); // NOI18N return context.getOffset() + 1; } @@ -242,7 +255,26 @@ class TypingCompletion { } if ((completablePosition && !insideString) || eol) { - context.setText(context.getText() + context.getText(), 1); + if (context.getText().equals("\"") && context.getOffset() >= 2 && + context.getDocument().getText(context.getOffset() - 2, 2).equals("\"\"") && + isTextBlockSupported(getFileObject((BaseDocument) context.getDocument()))) { + context.setText("\"\n\"\"\"", 2); // NOI18N + Thread.dumpStack(); + } else { + context.setText(context.getText() + context.getText(), 1); + } + } else if (context.getText().equals("\"") && + isTextBlockSupported(getFileObject((BaseDocument) context.getDocument()))) { + if ((javaTS != null) && javaTS.moveNext()) { + id = javaTS.token().id(); + if ((id == JavaTokenId.STRING_LITERAL) && (javaTS.token().text().toString().equals("\"\""))) { + if (context.getDocument().getText(context.getOffset(), 2).equals("\"\"")) { + context.setText("\"\"\"\n\"", 4); + } + } + javaTS.movePrevious(); + id = javaTS.token().id(); + } } return -1; } @@ -812,8 +844,13 @@ class TypingCompletion { } return null; } - - private static Set<JavaTokenId> STRING_AND_COMMENT_TOKENS = EnumSet.of(JavaTokenId.STRING_LITERAL, JavaTokenId.LINE_COMMENT, JavaTokenId.JAVADOC_COMMENT, JavaTokenId.BLOCK_COMMENT, JavaTokenId.CHAR_LITERAL); + + private static FileObject getFileObject(BaseDocument doc) { + DataObject dob = NbEditorUtilities.getDataObject(doc); + return dob.getPrimaryFile(); + } + + private static Set<JavaTokenId> STRING_AND_COMMENT_TOKENS = EnumSet.of(JavaTokenId.STRING_LITERAL, JavaTokenId.LINE_COMMENT, JavaTokenId.JAVADOC_COMMENT, JavaTokenId.BLOCK_COMMENT, JavaTokenId.CHAR_LITERAL, JavaTokenId.MULTILINE_STRING_LITERAL); private static boolean isStringOrComment(JavaTokenId javaTokenId) { return STRING_AND_COMMENT_TOKENS.contains(javaTokenId); diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/imports/ClipboardHandler.java b/java/java.editor/src/org/netbeans/modules/java/editor/imports/ClipboardHandler.java index 14adc77..0620499 100644 --- a/java/java.editor/src/org/netbeans/modules/java/editor/imports/ClipboardHandler.java +++ b/java/java.editor/src/org/netbeans/modules/java/editor/imports/ClipboardHandler.java @@ -89,6 +89,7 @@ import org.netbeans.api.java.source.GeneratorUtilities; import org.netbeans.api.java.source.JavaSource; import org.netbeans.api.java.source.ModificationResult; import org.netbeans.api.java.source.SourceUtils; +import static org.netbeans.api.java.source.SourceUtils.isTextBlockSupported; import org.netbeans.api.java.source.Task; import org.netbeans.api.java.source.WorkingCopy; import org.netbeans.api.lexer.TokenHierarchy; @@ -98,6 +99,7 @@ import org.netbeans.editor.BaseKit; import org.netbeans.editor.BaseKit.CutAction; import org.netbeans.lib.editor.util.swing.DocumentUtilities; import org.netbeans.modules.editor.NbEditorUtilities; +import org.netbeans.modules.editor.indent.api.IndentUtils; import org.openide.DialogDescriptor; import org.openide.DialogDisplayer; import org.openide.filesystems.FileObject; @@ -621,46 +623,102 @@ public class ClipboardHandler { private boolean delegatedImportData(final TransferSupport support) { JComponent comp = (JComponent) support.getComponent(); - if (comp instanceof JTextComponent && !support.isDataFlavorSupported(COPY_FROM_STRING_FLAVOR) && insideToken((JTextComponent) comp, JavaTokenId.STRING_LITERAL)) { - final Transferable t = support.getTransferable(); - return delegate.importData(comp, new Transferable() { - @Override - public DataFlavor[] getTransferDataFlavors() { - return t.getTransferDataFlavors(); - } + if (comp instanceof JTextComponent && !support.isDataFlavorSupported(COPY_FROM_STRING_FLAVOR) ) { + if (insideToken((JTextComponent) comp, JavaTokenId.STRING_LITERAL)) { + final Transferable t = support.getTransferable(); + return delegate.importData(comp, new Transferable() { + @Override + public DataFlavor[] getTransferDataFlavors() { + return t.getTransferDataFlavors(); + } - @Override - public boolean isDataFlavorSupported(DataFlavor flavor) { - return t.isDataFlavorSupported(flavor); - } + @Override + public boolean isDataFlavorSupported(DataFlavor flavor) { + return t.isDataFlavorSupported(flavor); + } - @Override - public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { - Object data = t.getTransferData(flavor); - if (data instanceof String) { - String s = (String) data; - s = s.replace("\\","\\\\"); //NOI18N - s = s.replace("\"","\\\""); //NOI18N - s = s.replace("\r\n","\n"); //NOI18N - s = s.replace("\n","\\n\" +\n\""); //NOI18N - data = s; - } else if (data instanceof Reader) { - BufferedReader br = new BufferedReader((Reader)data); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = br.readLine()) != null) { - line = line.replace("\\","\\\\"); //NOI18N - line = line.replace("\"","\\\""); //NOI18N - if (sb.length() > 0) { - sb.append("\\n\" +\n\""); //NOI18N + @Override + public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { + Object data = t.getTransferData(flavor); + if (data instanceof String) { + String s = (String) data; + s = s.replace("\\","\\\\"); //NOI18N + s = s.replace("\"","\\\""); //NOI18N + s = s.replace("\r\n","\n"); //NOI18N + s = s.replace("\n","\\n\" +\n\""); //NOI18N + data = s; + } else if (data instanceof Reader) { + BufferedReader br = new BufferedReader((Reader)data); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + line = line.replace("\\","\\\\"); //NOI18N + line = line.replace("\"","\\\""); //NOI18N + if (sb.length() > 0) { + sb.append("\\n\" +\n\""); //NOI18N + } + sb.append(line); } - sb.append(line); + data = new StringReader(sb.toString()); } - data = new StringReader(sb.toString()); + return data; } - return data; - } - }); + }); + } else if ((isTextBlockSupported(NbEditorUtilities.getFileObject(((JTextComponent)comp).getDocument()))) && insideToken((JTextComponent) comp, JavaTokenId.MULTILINE_STRING_LITERAL)) { + final Transferable t = support.getTransferable(); + return delegate.importData(comp, new Transferable() { + @Override + public DataFlavor[] getTransferDataFlavors() { + return t.getTransferDataFlavors(); + } + + @Override + public boolean isDataFlavorSupported(DataFlavor flavor) { + return t.isDataFlavorSupported(flavor); + } + + @Override + public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { + Object data = t.getTransferData(flavor); + JTextComponent c = (JTextComponent) comp; + int indent = 0; + try { + indent = IndentUtils.lineIndent(c.getDocument(), IndentUtils.lineStartOffset(c.getDocument(), c.getCaretPosition())); + } catch (BadLocationException ex) { + Exceptions.printStackTrace(ex); + } + if (data instanceof String) { + String s = (String) data; + s = s.replace("\"\"\"","\\\"\"\""); //NOI18N + StringBuilder sb = new StringBuilder(""); + for (int i = 0; i < indent; i++) { + sb.append(" "); //NOI18N + } + String emptySpaces = sb.toString(); + s = s.replace("\r\n","\n"); //NOI18N + s = s.replace("\n",System.lineSeparator() + emptySpaces); //NOI18N + data = s; + } else if (data instanceof Reader) { + BufferedReader br = new BufferedReader((Reader)data); + StringBuilder sb = new StringBuilder(); + String line; + + while ((line = br.readLine()) != null) { + line = line.replace("\"\"\"", "\\\"\"\""); //NOI18N + if (sb.length() > 0) { + sb.append(System.lineSeparator()); //NOI18N + for (int i = 0; i < indent; i++) { + sb.append(" "); //NOI18N + } + } + sb.append(line); + } + data = new StringReader(sb.toString()); + } + return data; + } + }); + } } return delegate.importData(support); } diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/resources/fontsColors.xml b/java/java.editor/src/org/netbeans/modules/java/editor/resources/fontsColors.xml index 33a873f..668ecb7 100644 --- a/java/java.editor/src/org/netbeans/modules/java/editor/resources/fontsColors.xml +++ b/java/java.editor/src/org/netbeans/modules/java/editor/resources/fontsColors.xml @@ -87,6 +87,7 @@ <fontcolor name="mod-protected" /> <fontcolor name="mod-public" /> <fontcolor name="mod-keyword" default="keyword" /> + <fontcolor name="mod-unindented-text-block" bgColor="EEDDDD" /> <!--currently not used:--> <!-- <fontcolor name="mod-type-parameter-declaration" bgColor="lightGray"/> diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ColoringManager.java b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ColoringManager.java index 5ffe5de..d5ec0cf 100644 --- a/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ColoringManager.java +++ b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ColoringManager.java @@ -85,6 +85,7 @@ public final class ColoringManager { put("mod-unused", UNUSED); put("mod-keyword", KEYWORD); put("javadoc-identifier", JAVADOC_IDENTIFIER); + put("mod-unindented-text-block", UNINDENTED_TEXT_BLOCK); } private static void put(String coloring, ColoringAttributes... attributes) { diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/semantic/SemanticHighlighter.java b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/SemanticHighlighter.java index 065d8f8..c143fc9 100644 --- a/java/java.editor/src/org/netbeans/modules/java/editor/semantic/SemanticHighlighter.java +++ b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/SemanticHighlighter.java @@ -43,6 +43,7 @@ import org.netbeans.modules.java.editor.base.semantic.SemanticHighlighterBase.Er import org.netbeans.spi.editor.highlighting.support.OffsetsBag; import org.netbeans.spi.editor.hints.ErrorDescription; import org.openide.loaders.DataObject; +import org.openide.util.Pair; /** * @@ -62,13 +63,12 @@ public class SemanticHighlighter extends SemanticHighlighterBase { public void setErrors(Document doc, List<ErrorDescription> errors, List<TreePathHandle> allUnusedImports) {} - public void setHighlights(final Document doc, final Collection<int[]> highlights, Map<int[], String> preText) { + public void setHighlights(final Document doc, final Collection<Pair<int[], Coloring>> highlights, Map<int[], String> preText) { SwingUtilities.invokeLater(new Runnable() { public void run() { OffsetsBag bag = new OffsetsBag(doc); - Coloring unused = ColoringAttributes.add(ColoringAttributes.empty(), ColoringAttributes.UNUSED); - for (int[] highlight : highlights) { - bag.addHighlight(highlight[0], highlight[1], ColoringManager.getColoringImpl(unused)); + for (Pair<int[], Coloring> highlight : highlights) { + bag.addHighlight(highlight.first()[0], highlight.first()[1], ColoringManager.getColoringImpl(highlight.second())); } getImportHighlightsBag(doc).setHighlights(bag); diff --git a/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/TypingCompletionUnitTest.java b/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/TypingCompletionUnitTest.java index 8345f5c..db4d1ff 100644 --- a/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/TypingCompletionUnitTest.java +++ b/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/TypingCompletionUnitTest.java @@ -23,6 +23,7 @@ import java.awt.EventQueue; import java.awt.event.KeyEvent; import java.util.prefs.Preferences; import java.util.regex.Pattern; +import javax.lang.model.SourceVersion; import javax.swing.JEditorPane; import javax.swing.SwingUtilities; import javax.swing.text.BadLocationException; @@ -1239,7 +1240,67 @@ public class TypingCompletionUnitTest extends NbTestCase { ctx.typeChar(')'); ctx.assertDocumentTextEquals("//()|)"); } - + + public void testTextBlock1() throws Exception { + try { + SourceVersion.valueOf("RELEASE_13"); + } catch (IllegalArgumentException ex) { + //OK, skip test + return ; + } + Context ctx = new Context(new JavaKit(), "\"\"|"); + ctx.typeChar('\"'); + ctx.assertDocumentTextEquals("\"\"\"\n|\"\"\""); + } + + public void testTextBlock2() throws Exception { + try { + SourceVersion.valueOf("RELEASE_13"); + } catch (IllegalArgumentException ex) { + //OK, skip test + return ; + } + Context ctx = new Context(new JavaKit(), "\"\"\"\n|\"\"\""); + ctx.typeChar('\"'); + ctx.assertDocumentTextEquals("\"\"\"\n\"|\"\""); + } + + public void testTextBlock3() throws Exception { + try { + SourceVersion.valueOf("RELEASE_13"); + } catch (IllegalArgumentException ex) { + //OK, skip test + return ; + } + Context ctx = new Context(new JavaKit(), "\"\"\"\n\"|\"\""); + ctx.typeChar('\"'); + ctx.assertDocumentTextEquals("\"\"\"\n\"\"|\""); + } + + public void testTextBlock4() throws Exception { + try { + SourceVersion.valueOf("RELEASE_13"); + } catch (IllegalArgumentException ex) { + //OK, skip test + return ; + } + Context ctx = new Context(new JavaKit(), "\"\"\"\n\"\"|\""); + ctx.typeChar('\"'); + ctx.assertDocumentTextEquals("\"\"\"\n\"\"\"|"); + } + + public void testTextBlock5() throws Exception { + try { + SourceVersion.valueOf("RELEASE_13"); + } catch (IllegalArgumentException ex) { + //OK, skip test: + return ; + } + Context ctx = new Context(new JavaKit(), "t(|\"\")"); + ctx.typeChar('\"'); + ctx.assertDocumentTextEquals("t(\"\"\"\n |\"\"\")"); + } + public void testCorrectHandlingOfStringEscapes184059() throws Exception { assertTrue(isInsideString("foo\n\"bar|\"")); assertTrue(isInsideString("foo\n\"bar\\\"|\"")); diff --git a/java/java.editor/test/unit/src/org/netbeans/modules/java/editor/imports/ClipboardHandlerTest.java b/java/java.editor/test/unit/src/org/netbeans/modules/java/editor/imports/ClipboardHandlerTest.java index cab88de..3c43513 100644 --- a/java/java.editor/test/unit/src/org/netbeans/modules/java/editor/imports/ClipboardHandlerTest.java +++ b/java/java.editor/test/unit/src/org/netbeans/modules/java/editor/imports/ClipboardHandlerTest.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.util.concurrent.Exchanger; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; +import javax.lang.model.SourceVersion; import javax.swing.JEditorPane; import javax.swing.SwingUtilities; import org.netbeans.api.java.lexer.JavaTokenId; @@ -90,6 +91,20 @@ public class ClipboardHandlerTest extends NbTestCase { public void testAnonymousClass() throws Exception { copyAndPaste("package test;\nimport java.util.ArrayList;\npublic class Test { void t() { |new ArrayList<String>() {};| } }\n", "package test;\npublic class Target {\nvoid t() { ^ }\n}", "package test;\n\nimport java.util.ArrayList;\n\npublic class Target {\nvoid t() { new ArrayList<String>() {}; }\n}"); } + + public void testCopyIntoTextBlock() throws Exception { + copyAndPaste("|List l1;\nList l2;\nList l3;\n\n| ", "package test;\npublic class Target {\nString s = \"\"\"\n^\"\"\"\n}", "package test;\npublic class Target {\nString s = \"\"\"\nList l1;\nList l2;\nList l3;\n\n\"\"\"\n}"); + } + + public void testCopyTextBlockIntoTextBlock() throws Exception { + try { + SourceVersion.valueOf("RELEASE_13"); + } catch (IllegalArgumentException ex) { + //OK, skip test + return ; + } + copyAndPaste("|\"\"\"\nList l1;\"\"\"| ", "package test;\npublic class Target {\nString s = \"\"\"\ntest^ block\n\"\"\"\n}", "package test;\npublic class Target {\nString s = \"\"\"\ntest\\\"\"\"\nList l1;\\\"\"\" block\n\"\"\"\n}"); + } private void copyAndPaste(String from, final String to, String golden) throws Exception { final int pastePos = to.indexOf('^'); diff --git a/java/java.hints/src/org/netbeans/modules/java/hints/jdk/ConvertToTextBlock.java b/java/java.hints/src/org/netbeans/modules/java/hints/jdk/ConvertToTextBlock.java new file mode 100644 index 0000000..35f0917 --- /dev/null +++ b/java/java.hints/src/org/netbeans/modules/java/hints/jdk/ConvertToTextBlock.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.java.hints.jdk; + +import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.LiteralTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.Tree.Kind; +import com.sun.source.util.TreePath; +import java.util.List; +import org.netbeans.api.java.queries.CompilerOptionsQuery; +import org.netbeans.api.java.source.CompilationInfo; +import org.netbeans.spi.editor.hints.ErrorDescription; +import org.netbeans.spi.editor.hints.Fix; +import org.netbeans.spi.java.hints.ConstraintVariableType; +import org.netbeans.spi.java.hints.ErrorDescriptionFactory; +import org.netbeans.spi.java.hints.Hint; +import org.netbeans.spi.java.hints.HintContext; +import org.netbeans.spi.java.hints.JavaFix; +import org.netbeans.spi.java.hints.TriggerPattern; +import org.netbeans.spi.java.hints.TriggerTreeKind; +import org.openide.util.NbBundle.Messages; + +@Hint(displayName = "#DN_ConvertToTextBlock", description = "#DESC_ConvertToTextBlock", category="rules15", + minSourceVersion = "13") +@Messages({ + "DN_ConvertToTextBlock=Convert to Text Block", + "DESC_ConvertToTextBlock=Convert to Text Block" +}) +public class ConvertToTextBlock { + + @TriggerTreeKind(Kind.PLUS) + @Messages("ERR_ConvertToTextBlock=Can be converted to text block") + public static ErrorDescription computeWarning(HintContext ctx) { + if (!CompilerOptionsQuery.getOptions(ctx.getInfo().getFileObject()).getArguments().contains("--enable-preview")) + return null; + if (ctx.getPath().getParentPath() != null && getTextOrNull(ctx.getPath().getParentPath()) != null) { + return null; + } + String text = getTextOrNull(ctx.getPath()); + if (text == null) { + return null; + } + Fix fix = new FixImpl(ctx.getInfo(), ctx.getPath(), text).toEditorFix(); + return ErrorDescriptionFactory.forName(ctx, ctx.getPath(), Bundle.ERR_ConvertToTextBlock(), fix); + } + + private static String getTextOrNull(TreePath tp) { + StringBuilder text = new StringBuilder(); + Tree current = tp.getLeaf(); + while (current.getKind() == Kind.PLUS) { + BinaryTree bt = (BinaryTree) current; + if (bt.getRightOperand().getKind() == Kind.STRING_LITERAL) { + text.insert(0, ((LiteralTree) bt.getRightOperand()).getValue()); + } else { + return null; + } + current = bt.getLeftOperand(); + } + if (current.getKind() == Kind.STRING_LITERAL) { + text.insert(0, ((LiteralTree) current).getValue()); + } else { + return null; + } + String textString = text.toString(); + if (!textString.contains("\n")) { + return null; + } + return textString; + } + + private static final class FixImpl extends JavaFix { + + private final String text; + + public FixImpl(CompilationInfo info, TreePath tp, String text) { + super(info, tp); + this.text = text; + } + + @Override + @Messages("FIX_ConvertToTextBlock=Convert to text block") + protected String getText() { + return Bundle.FIX_ConvertToTextBlock(); + } + + @Override + protected void performRewrite(TransformationContext ctx) { + ctx.getWorkingCopy().rewrite(ctx.getPath().getLeaf(), ctx.getWorkingCopy().getTreeMaker().Literal(text.split("\n", -1))); + //perform the required transformation + } + } +} diff --git a/java/java.hints/src/org/netbeans/modules/java/hints/resources/jdk13.properties b/java/java.hints/src/org/netbeans/modules/java/hints/resources/jdk13.properties new file mode 100644 index 0000000..532a56d --- /dev/null +++ b/java/java.hints/src/org/netbeans/modules/java/hints/resources/jdk13.properties @@ -0,0 +1,17 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +display.name=Migrate to JDK 13 diff --git a/java/java.hints/src/org/netbeans/modules/java/hints/resources/layer.xml b/java/java.hints/src/org/netbeans/modules/java/hints/resources/layer.xml index 689379e..8e70d6a 100644 --- a/java/java.hints/src/org/netbeans/modules/java/hints/resources/layer.xml +++ b/java/java.hints/src/org/netbeans/modules/java/hints/resources/layer.xml @@ -398,11 +398,27 @@ <file name="org.netbeans.modules.java.hints.jdk.ConvertToVarHint.properties" url="enabled.properties"/> <file name="org.netbeans.modules.java.hints.jdk.ConvertSwitchToRuleSwitch.properties" url="enabled.properties"/> </folder> + <folder name="rule_config_jdk13"> + <file name="Javac_canUseDiamond.properties" url="enabled.properties"/> + <file name="Javac_canUseLambda.properties" url="enabled.properties"/> + <file name="org.netbeans.modules.java.hints.jdk.IteratorToFor.properties" url="enabled.properties"/> + <file name="org.netbeans.modules.java.hints.jdk.UnnecessaryUnboxing.properties" url="enabled.properties"/> + <file name="org.netbeans.modules.java.hints.jdk.ConvertToARM.properties" url="enabled.properties"/> + <file name="org.netbeans.modules.java.hints.jdk.JoinCatches.properties" url="enabled.properties"/> + <file name="org.netbeans.modules.java.hints.jdk.Tiny.containsForIndexOf.properties" url="enabled.properties"/> + <file name="org.netbeans.modules.java.hints.StaticImport.properties" url="enabled.properties"/> + <file name="org.netbeans.modules.java.hints.jdk.ConvertToStringSwitch.properties" url="enabled.properties"/> + <file name="org.netbeans.modules.java.hints.jdk.UnnecessaryBoxing.run.properties" url="enabled.properties"/> + <file name="org.netbeans.modules.java.hints.jdk.ConvertToVarHint.properties" url="enabled.properties"/> + <file name="org.netbeans.modules.java.hints.jdk.ConvertSwitchToRuleSwitch.properties" url="enabled.properties"/> + <file name="org.netbeans.modules.java.hints.jdk.ConvertToTextBlock.properties" url="enabled.properties"/> + </folder> <folder name="ui"> <file name="rule_config_default.properties" url="nbresloc:/org/netbeans/modules/java/hints/resources/default.properties"/> <file name="rule_config_jdk8.properties" url="nbresloc:/org/netbeans/modules/java/hints/resources/jdk8.properties"/> <file name="rule_config_jdk11.properties" url="nbresloc:/org/netbeans/modules/java/hints/resources/jdk11.properties"/> <file name="rule_config_jdk12.properties" url="nbresloc:/org/netbeans/modules/java/hints/resources/jdk12.properties"/> + <file name="rule_config_jdk13.properties" url="nbresloc:/org/netbeans/modules/java/hints/resources/jdk13.properties"/> </folder> </folder></folder></folder></folder></folder> </folder> diff --git a/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/jdk/ConvertToTextBlockTest.java b/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/jdk/ConvertToTextBlockTest.java new file mode 100644 index 0000000..91749bd --- /dev/null +++ b/java/java.hints/test/unit/src/org/netbeans/modules/java/hints/jdk/ConvertToTextBlockTest.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.java.hints.jdk; + +import javax.lang.model.SourceVersion; +import org.junit.Test; +import org.netbeans.modules.java.hints.test.api.HintTest; + +public class ConvertToTextBlockTest { + + @Test + public void testFixWorking() throws Exception { + try { + SourceVersion.valueOf("RELEASE_13"); + } catch (IllegalArgumentException ex) { + //OK, skip test + return ; + } + HintTest.create() + .input("package test;\n" + + "public class Test {\n" + + " public static void main(String[] args) {\n" + + " assert args[0].equals(\"{\\n\" +\n" + + " \" int i = 0;\\n\" +\n" + + " \"}\");\n" + + " }\n" + + "}\n") + .sourceLevel(SourceVersion.latest().name()) + .options("--enable-preview") + .run(ConvertToTextBlock.class) + .findWarning("3:30-3:37:verifier:" + Bundle.ERR_ConvertToTextBlock()) + .applyFix() + .assertCompilable() + //TODO: change to match expected output + .assertOutput("package test;\n" + + "public class Test {\n" + + " public static void main(String[] args) {\n" + + " assert args[0].equals(\"\"\"\n" + + " {\n" + + " int i = 0;\n" + + " }\"\"\");\n" + + " }\n" + + "}\n"); + } + + @Test + public void testNewLineAtEnd() throws Exception { + try { + SourceVersion.valueOf("RELEASE_13"); + } catch (IllegalArgumentException ex) { + //OK, skip test + return ; + } + HintTest.create() + .input("package test;\n" + + "public class Test {\n" + + " public static void main(String[] args) {\n" + + " assert args[0].equals(\"{\\n\" +\n" + + " \" int i = 0;\\n\" +\n" + + " \"}\\n\");\n" + + " }\n" + + "}\n") + .sourceLevel(SourceVersion.latest().name()) + .options("--enable-preview") + .run(ConvertToTextBlock.class) + .findWarning("3:30-3:37:verifier:" + Bundle.ERR_ConvertToTextBlock()) + .applyFix() + .assertCompilable() + //TODO: change to match expected output + .assertOutput("package test;\n" + + "public class Test {\n" + + " public static void main(String[] args) {\n" + + " assert args[0].equals(\"\"\"\n" + + " {\n" + + " int i = 0;\n" + + " }\n" + + " \"\"\");\n" + + " }\n" + + "}\n"); + } + + @Test + public void testNewLinesAtEnd() throws Exception { + try { + SourceVersion.valueOf("RELEASE_13"); + } catch (IllegalArgumentException ex) { + //OK, skip test + return ; + } + HintTest.create() + .input("package test;\n" + + "public class Test {\n" + + " public static void main(String[] args) {\n" + + " assert args[0].equals(\"{\\n\" +\n" + + " \" int i = 0;\\n\" +\n" + + " \"}\\n\\n\");\n" + + " }\n" + + "}\n") + .sourceLevel(SourceVersion.latest().name()) + .options("--enable-preview") + .run(ConvertToTextBlock.class) + .findWarning("3:30-3:37:verifier:" + Bundle.ERR_ConvertToTextBlock()) + .applyFix() + .assertCompilable() + .assertOutput("package test;\n" + + "public class Test {\n" + + " public static void main(String[] args) {\n" + + " assert args[0].equals(\"\"\"\n" + + " {\n" + + " int i = 0;\n" + + " }\n" + + " \n" + + " \"\"\");\n" + + " }\n" + + "}\n"); + } + + @Test + public void testOnlyLiterals() throws Exception { + try { + SourceVersion.valueOf("RELEASE_13"); + } catch (IllegalArgumentException ex) { + //OK, skip test + return ; + } + HintTest.create() + .input("package test;\n" + + "public class Test {\n" + + " public int test() {\n" + + " return c() + c();\n" + + " }\n" + + " private int c() { return 0; }\n" + + "}\n") + .sourceLevel(SourceVersion.latest().name()) + .options("--enable-preview") + .run(ConvertToTextBlock.class) + .assertWarnings(); + } +} diff --git a/java/java.lexer/apichanges.xml b/java/java.lexer/apichanges.xml index 77a4f2a..c8ceebf 100644 --- a/java/java.lexer/apichanges.xml +++ b/java/java.lexer/apichanges.xml @@ -83,6 +83,18 @@ is the proper place. <!-- ACTUAL CHANGES BEGIN HERE: --> <changes> + <change id="RawStringLiteral"> + <api name="general"/> + <summary>Added RAW_STRING_LITERAL JavaTokenKind</summary> + <version major="1" minor="41"/> + <date day="22" month="4" year="2018"/> + <author login="jlahoda"/> + <compatibility addition="yes" binary="compatible" deletion="no" deprecation="no" modification="no" semantic="compatible" source="compatible"/> + <description> + Added JavaTokenId.RAW_STRING_LITERAL. + </description> + <class package="org.netbeans.api.java.lexer" name="JavaTokenId"/> + </change> <change id="Modules"> <api name="general"/> <summary>Added tokens used in module-info.java files</summary> diff --git a/java/java.lexer/src/org/netbeans/api/java/lexer/JavaTokenId.java b/java/java.lexer/src/org/netbeans/api/java/lexer/JavaTokenId.java index 7cf0fbb..17602a5 100644 --- a/java/java.lexer/src/org/netbeans/api/java/lexer/JavaTokenId.java +++ b/java/java.lexer/src/org/netbeans/api/java/lexer/JavaTokenId.java @@ -127,6 +127,8 @@ public enum JavaTokenId implements TokenId { DOUBLE_LITERAL(null, "number"), CHAR_LITERAL(null, "character"), STRING_LITERAL(null, "string"), + /**@since 1.41*/ + MULTILINE_STRING_LITERAL(null, "string"), TRUE("true", "literal"), FALSE("false", "literal"), diff --git a/java/java.lexer/src/org/netbeans/lib/java/lexer/JavaLexer.java b/java/java.lexer/src/org/netbeans/lib/java/lexer/JavaLexer.java index d0a598d..8a2b651 100644 --- a/java/java.lexer/src/org/netbeans/lib/java/lexer/JavaLexer.java +++ b/java/java.lexer/src/org/netbeans/lib/java/lexer/JavaLexer.java @@ -170,19 +170,41 @@ public class JavaLexer implements Lexer<JavaTokenId> { return token(JavaTokenId.ERROR); case '"': // string literal if (lookupId == null) lookupId = JavaTokenId.STRING_LITERAL; - while (true) + while (true) { switch (nextChar()) { case '"': // NOI18N + if (this.version >= 13) { + String text = input.readText().toString(); + if (text.length() == 2) { + if (nextChar() != '"') { + input.backup(1); //TODO: EOF??? + return token(lookupId); + } + lookupId = JavaTokenId.MULTILINE_STRING_LITERAL; + } + if (lookupId == JavaTokenId.MULTILINE_STRING_LITERAL) { + if (text.endsWith("\"\"\"") && !text.endsWith("\\\"\"\"") && text.length() > 6) { + return token(lookupId); + } else { + break; + } + } + } + return token(lookupId); case '\\': nextChar(); break; case '\r': consumeNewline(); case '\n': + if (lookupId == JavaTokenId.MULTILINE_STRING_LITERAL && this.version >= 13) { + break; + } case EOF: return tokenFactory.createToken(lookupId, //XXX: \n handling for exotic identifiers? input.readLength(), PartType.START); } + } case '\'': // char literal while (true) diff --git a/java/java.lexer/test/unit/src/org/netbeans/lib/java/lexer/JavaLexerBatchTest.java b/java/java.lexer/test/unit/src/org/netbeans/lib/java/lexer/JavaLexerBatchTest.java index b87cbdd..757ae28 100644 --- a/java/java.lexer/test/unit/src/org/netbeans/lib/java/lexer/JavaLexerBatchTest.java +++ b/java/java.lexer/test/unit/src/org/netbeans/lib/java/lexer/JavaLexerBatchTest.java @@ -106,12 +106,13 @@ public class JavaLexerBatchTest extends TestCase { } public void testStringLiterals() { - String text = "\"\" \"a\"\"\" \"\\\"\" \"\\\\\" \"\\\\\\\"\" \"\\n\" \"a"; + String text = "\"\" \"a\" \"\" \"\\\"\" \"\\\\\" \"\\\\\\\"\" \"\\n\" \"a"; TokenHierarchy<?> hi = TokenHierarchy.create(text, JavaTokenId.language()); TokenSequence<?> ts = hi.tokenSequence(); LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.STRING_LITERAL, "\"\""); LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.WHITESPACE, " "); LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.STRING_LITERAL, "\"a\""); + LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.WHITESPACE, " "); LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.STRING_LITERAL, "\"\""); LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.WHITESPACE, " "); LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.STRING_LITERAL, "\"\\\"\""); @@ -697,4 +698,36 @@ public class JavaLexerBatchTest extends TestCase { LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.WHITESPACE, " "); } + private void enableRawStringLiterals(InputAttributes attr) { + attr.setValue(JavaTokenId.language(), "version", (Supplier<String>) () -> { + return "13"; + }, true); + } + + public void testMultilineLiteral() { + String text = "\"\"\"\n2\"3\n4\\\"\"\"5\"\"\\\"6\n7\"\"\" \"\"\"wrong\"\"\" \"\"\"\nbla\n"; + InputAttributes attr = new InputAttributes(); + enableRawStringLiterals(attr); + TokenHierarchy<?> hi = TokenHierarchy.create(text, false, JavaTokenId.language(), EnumSet.noneOf(JavaTokenId.class), attr); + TokenSequence<?> ts = hi.tokenSequence(); + + LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.MULTILINE_STRING_LITERAL, "\"\"\"\n2\"3\n4\\\"\"\"5\"\"\\\"6\n7\"\"\""); + LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.WHITESPACE, " "); + LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.MULTILINE_STRING_LITERAL, "\"\"\"wrong\"\"\""); + LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.WHITESPACE, " "); + LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.MULTILINE_STRING_LITERAL, "\"\"\"\nbla\n"); + assertFalse(ts.moveNext()); + } + + public void testTrailing() { + String text = "\"\""; + InputAttributes attr = new InputAttributes(); + enableRawStringLiterals(attr); + TokenHierarchy<?> hi = TokenHierarchy.create(text, false, JavaTokenId.language(), EnumSet.noneOf(JavaTokenId.class), attr); + TokenSequence<?> ts = hi.tokenSequence(); + + LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.STRING_LITERAL, "\"\""); + assertFalse(ts.moveNext()); + } + } diff --git a/java/java.source.base/src/org/netbeans/api/java/source/SourceUtils.java b/java/java.source.base/src/org/netbeans/api/java/source/SourceUtils.java index 8e136e2..fa7bbb4 100644 --- a/java/java.source.base/src/org/netbeans/api/java/source/SourceUtils.java +++ b/java/java.source.base/src/org/netbeans/api/java/source/SourceUtils.java @@ -80,13 +80,16 @@ import javax.tools.JavaFileObject; import org.netbeans.api.annotations.common.CheckForNull; import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.annotations.common.NullAllowed; import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.api.editor.mimelookup.MimePath; import org.netbeans.api.java.classpath.ClassPath; import org.netbeans.api.java.classpath.JavaClassPathConstants; import org.netbeans.api.java.lexer.JavaTokenId; +import org.netbeans.api.java.queries.CompilerOptionsQuery; import org.netbeans.api.java.queries.JavadocForBinaryQuery; import org.netbeans.api.java.queries.SourceForBinaryQuery; +import org.netbeans.api.java.queries.SourceLevelQuery; import org.netbeans.api.java.source.ClasspathInfo.PathKind; import org.netbeans.api.java.source.JavaSource.Phase; import org.netbeans.api.java.source.matching.Matcher; @@ -120,6 +123,7 @@ import org.netbeans.spi.java.classpath.support.ClassPathSupport; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.filesystems.URLMapper; +import org.openide.modules.SpecificationVersion; import org.openide.util.Exceptions; import org.openide.util.Lookup; import org.openide.util.Pair; @@ -563,7 +567,27 @@ public class SourceUtils { } return null; } - + + /** + * @since 13.0 + */ + public static boolean isTextBlockSupported( + @NullAllowed FileObject fileObject) { + SpecificationVersion supportedVer = new SpecificationVersion("13"); //NOI18N + + SpecificationVersion runtimeVer = new SpecificationVersion(System.getProperty("java.specification.version")); //NOI18N + if (runtimeVer.compareTo(supportedVer) >= 0) { + if (fileObject != null) { + SpecificationVersion sourceVer = new SpecificationVersion(SourceLevelQuery.getSourceLevel(fileObject)); + if (sourceVer.compareTo(supportedVer) < 0) { + return false; + } + } + return true; + } + return false; + } + private static FileObject findSourceForBinary(FileObject binaryRoot, FileObject binary, String signature, String pkgName, String className, boolean isPkg) throws IOException { FileObject[] sourceRoots = SourceForBinaryQuery.findSourceRoots(binaryRoot.toURL()).getRoots(); ClassPath sourcePath = ClassPathSupport.createClassPath(sourceRoots); diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/builder/TreeFactory.java b/java/java.source.base/src/org/netbeans/modules/java/source/builder/TreeFactory.java index 0ee4288..1e89e99 100644 --- a/java/java.source.base/src/org/netbeans/modules/java/source/builder/TreeFactory.java +++ b/java/java.source.base/src/org/netbeans/modules/java/source/builder/TreeFactory.java @@ -507,6 +507,8 @@ public class TreeFactory { return make.at(NOPOS).Literal(TypeTag.INT, ((Byte) value).intValue()); if (value instanceof Short) return make.at(NOPOS).Literal(TypeTag.INT, ((Short) value).intValue()); + if (value instanceof String[]) + return make.at(NOPOS).Literal(TypeTag.CLASS, value); // workaround for making NULL_LITERAL kind. if (value == null) { return make.at(NOPOS).Literal(TypeTag.BOT, value); diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/pretty/CharBuffer.java b/java/java.source.base/src/org/netbeans/modules/java/source/pretty/CharBuffer.java index 0f9a739..14eff95 100644 --- a/java/java.source.base/src/org/netbeans/modules/java/source/pretty/CharBuffer.java +++ b/java/java.source.base/src/org/netbeans/modules/java/source/pretty/CharBuffer.java @@ -235,17 +235,29 @@ public final class CharBuffer { public void addTrimObserver(TrimBufferObserver o) { trimObservers.add(o); } + public void nlTerm() { + nlTerm(true); + } + + public void nlTerm(boolean trim) { if(hasMargin()) needSpace(); else { int t = used; if (t <= 0) return; - while (t > 0 && chars[t-1] <= ' ') t--; // NOI18N - trimTo(t); + if (trim) { + while (t > 0 && chars[t-1] <= ' ') t--; // NOI18N + trimTo(t); + } append('\n'); // NOI18N } } + + public void nlTermNoTrim() { + nlTerm(false); + } + public void toLineStart() { if(hasMargin()) needSpace(); diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/pretty/VeryPretty.java b/java/java.source.base/src/org/netbeans/modules/java/source/pretty/VeryPretty.java index 5231dbc..66c0043 100644 --- a/java/java.source.base/src/org/netbeans/modules/java/source/pretty/VeryPretty.java +++ b/java/java.source.base/src/org/netbeans/modules/java/source/pretty/VeryPretty.java @@ -102,6 +102,7 @@ import org.netbeans.api.java.source.CodeStyle; import org.netbeans.api.java.source.CodeStyle.*; import org.netbeans.api.java.source.Comment; import org.netbeans.api.java.source.Comment.Style; +import static org.netbeans.api.java.source.SourceUtils.isTextBlockSupported; import org.netbeans.api.lexer.TokenHierarchy; import org.netbeans.api.lexer.TokenSequence; import org.netbeans.modules.java.source.TreeShims; @@ -240,6 +241,10 @@ public final class VeryPretty extends JCTree.Visitor implements DocTreeVisitor<V out.nlTerm(); } + private void newLineNoTrim() { + out.nlTermNoTrim(); + } + public void blankline() { out.blanklines(1); } @@ -1873,7 +1878,34 @@ public final class VeryPretty extends JCTree.Visitor implements DocTreeVisitor<V "\'"); break; case CLASS: - print("\"" + quote((String) tree.value, '\'') + "\""); + if (tree.value instanceof String) { + print("\"" + quote((String) tree.value, '\'') + "\""); + } else if (isTextBlockSupported(null) && tree.value instanceof String[]) { + int indent = out.col; + print("\"\"\""); + newline(); + String[] lines = (String[]) tree.value; + for (int i = 0; i < lines.length; i++) { + out.toCol(indent); + String line = lines[i]; + for (int c = 0; c < line.length(); c++) { + if (line.startsWith("\"\"\"", c)) { + print('\\'); + print('"'); + } else if (line.charAt(c) != '\'' && line.charAt(c) != '"') { + print(Convert.quote(line.charAt(c))); + } else { + print(line.charAt(c)); + } + } + if (i + 1 < lines.length) { + newLineNoTrim(); + } + } + print("\"\"\""); + } else { + throw new IllegalStateException("Incorrect literal value."); + } break; case BOOLEAN: print(tree.getValue().toString()); diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/save/Reindenter.java b/java/java.source.base/src/org/netbeans/modules/java/source/save/Reindenter.java index 597c093..092df1c 100644 --- a/java/java.source.base/src/org/netbeans/modules/java/source/save/Reindenter.java +++ b/java/java.source.base/src/org/netbeans/modules/java/source/save/Reindenter.java @@ -68,10 +68,12 @@ import javax.tools.JavaFileObject; import com.sun.tools.javac.parser.ParserFactory; import com.sun.tools.javac.util.Log; import java.util.ArrayList; +import java.util.Arrays; import org.netbeans.api.java.lexer.JavaTokenId; import org.netbeans.api.java.source.CodeStyle; import org.netbeans.api.lexer.TokenHierarchy; import org.netbeans.api.lexer.TokenSequence; +import org.netbeans.modules.editor.indent.api.IndentUtils; import org.netbeans.modules.editor.indent.spi.Context; import org.netbeans.modules.editor.indent.spi.Context.Region; import org.netbeans.modules.editor.indent.spi.ExtraLock; @@ -116,6 +118,7 @@ public class Reindenter implements IndentTask { for (Region region : context.indentRegions()) { if (initRegionData(region)) { HashSet<Integer> linesToAddStar = new HashSet<Integer>(); + Map<Integer, Integer> oldIndents = new HashMap<>(); LinkedList<Integer> startOffsets = getStartOffsets(region); for (ListIterator<Integer> it = startOffsets.listIterator(); it.hasNext();) { int startOffset = it.next(); @@ -151,6 +154,17 @@ public class Reindenter implements IndentTask { if (!blockCommentLine.startsWith("*")) { //NOI18N linesToAddStar.add(startOffset); } + } else if (ts.token().id() == JavaTokenId.MULTILINE_STRING_LITERAL) { + String tokenText = ts.token().text().toString(); + String[] lines = tokenText.split("\n"); + int indent = Arrays.stream(lines, 1, lines.length) + .mapToInt(this::leadingIndent) + .min() + .orElse(0); + int initialLineStartOffset = context.lineStartOffset(ts.offset()); + int indentUpdate = newIndents.getOrDefault(initialLineStartOffset, 0); + oldIndents.put(startOffset, indent); + newIndents.put(startOffset, ts.offset() - initialLineStartOffset + indentUpdate); } else { if (delta == 0 && ts.moveNext() && ts.token().id() == JavaTokenId.LINE_COMMENT) { newIndents.put(startOffset, 0); @@ -168,7 +182,12 @@ public class Reindenter implements IndentTask { context.document().insertString(startOffset, "* ", null); //NOI18N } if (newIndent != null) { - context.modifyIndent(startOffset, newIndent); + Integer oldIndent = oldIndents.get(startOffset); + if (oldIndent != null) { + context.modifyIndent(startOffset, oldIndent, IndentUtils.createIndentString(newIndent, true, -1)); + } else { + context.modifyIndent(startOffset, newIndent); + } } if (!startOffsets.isEmpty()) { char c; @@ -185,6 +204,19 @@ public class Reindenter implements IndentTask { } } + private int leadingIndent(String line) { + int indent = 0; + + for (int i = 0; i < line.length(); i++) { //TODO: code points + if (Character.isWhitespace(line.charAt(i))) + indent++; + else + break; + } + + return indent; + } + @Override public ExtraLock indentLock() { return null; diff --git a/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/LiteralTest.java b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/LiteralTest.java index 4f07d54..6feed6a 100644 --- a/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/LiteralTest.java +++ b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/LiteralTest.java @@ -25,6 +25,7 @@ import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.VariableTree; import java.io.File; +import javax.lang.model.SourceVersion; import org.netbeans.api.java.source.Task; import org.netbeans.api.java.source.JavaSource; import org.netbeans.api.java.source.JavaSource.Phase; @@ -168,6 +169,93 @@ public class LiteralTest extends GeneratorTestMDRCompat { assertEquals(golden, res); } + public void testTextBlocksNew() throws Exception { + try { + SourceVersion.valueOf("RELEASE_13"); + } catch (IllegalArgumentException ex) { + //OK, skip test: + return ; + } + testFile = new File(getWorkDir(), "Test.java"); + TestUtilities.copyStringToFile(testFile, + "package hierbas.del.litoral;\n" + + "\n" + + "public class Test {\n" + + " public static final String C;\n" + + "}\n" + ); + String golden = + "package hierbas.del.litoral;\n" + + "\n" + + "public class Test {\n" + + " public static final String C = \"\"\"\n" + + " line \"1\n" + + " line\\\"\\\"\\\"\"\"2\"\n" + + " line\\n3\"\"\";\n" + + "}\n"; + JavaSource testSource = JavaSource.forFileObject(FileUtil.toFileObject(testFile)); + Task<WorkingCopy> task = new Task<WorkingCopy>() { + + public void run(WorkingCopy workingCopy) throws java.io.IOException { + workingCopy.toPhase(Phase.RESOLVED); + TreeMaker make = workingCopy.getTreeMaker(); + + ClassTree clazz = (ClassTree) workingCopy.getCompilationUnit().getTypeDecls().get(0); + VariableTree var = (VariableTree) clazz.getMembers().get(1); + LiteralTree val = make.Literal(new String[] {"line \"1", + "line\"\"\"\"\"2\"", + "line\n3"}); + VariableTree nue = make.setInitialValue(var, val); + workingCopy.rewrite(var, nue); + } + + }; + testSource.runModificationTask(task).commit(); + String res = TestUtilities.copyFileToString(testFile); + System.err.println(res); + assertEquals(golden, res); + } + + public void testTextBlocksReplace() throws Exception { + testFile = new File(getWorkDir(), "Test.java"); + TestUtilities.copyStringToFile(testFile, + "package hierbas.del.litoral;\n" + + "\n" + + "public class Test {\n" + + " public static final String C = \"\"\"\n" + + " old\"\"\";\n" + + "}\n" + ); + String golden = + "package hierbas.del.litoral;\n" + + "\n" + + "public class Test {\n" + + " public static final String C = \"\"\"\n" + + " new\n" + + " \"\"\";\n" + + "}\n"; + JavaSource testSource = JavaSource.forFileObject(FileUtil.toFileObject(testFile)); + Task<WorkingCopy> task = new Task<WorkingCopy>() { + + public void run(WorkingCopy workingCopy) throws java.io.IOException { + workingCopy.toPhase(Phase.RESOLVED); + TreeMaker make = workingCopy.getTreeMaker(); + + ClassTree clazz = (ClassTree) workingCopy.getCompilationUnit().getTypeDecls().get(0); + VariableTree var = (VariableTree) clazz.getMembers().get(1); + LiteralTree val = make.Literal(new String[] {"new", + ""}); + VariableTree nue = make.setInitialValue(var, val); + workingCopy.rewrite(var, nue); + } + + }; + testSource.runModificationTask(task).commit(); + String res = TestUtilities.copyFileToString(testFile); + System.err.println(res); + assertEquals(golden, res); + } + String getGoldenPckg() { return ""; } diff --git a/java/java.source.base/test/unit/src/org/netbeans/modules/java/source/save/ReindenterTest.java b/java/java.source.base/test/unit/src/org/netbeans/modules/java/source/save/ReindenterTest.java index 4c49ae7..3848fb4 100644 --- a/java/java.source.base/test/unit/src/org/netbeans/modules/java/source/save/ReindenterTest.java +++ b/java/java.source.base/test/unit/src/org/netbeans/modules/java/source/save/ReindenterTest.java @@ -20,6 +20,7 @@ package org.netbeans.modules.java.source.save; import java.util.prefs.Preferences; import java.util.regex.Pattern; +import javax.lang.model.SourceVersion; import javax.swing.text.Document; import javax.swing.text.PlainDocument; import org.netbeans.api.editor.mimelookup.MimeLookup; @@ -1943,7 +1944,39 @@ public class ReindenterTest extends NbTestCase { "package t;\npublic class T {\n public void op() {\n int a = switch(get())\n {\n }\n }\n}\n"); } - + public void testNewLineIndentationTextBlock1() throws Exception { + try { + SourceVersion.valueOf("RELEASE_13"); + } catch (IllegalArgumentException ex) { + //OK, skip test: + return ; + } + performNewLineIndentationTest("package t;\npublic class T {\n private final String s = \"\"\"|\n}\n", + "package t;\npublic class T {\n private final String s = \"\"\"\n \n}\n"); + } + + public void testSpanIndentationTextBlock1() throws Exception { + try { + SourceVersion.valueOf("RELEASE_13"); + } catch (IllegalArgumentException ex) { + //OK, skip test: + return ; + } + performSpanIndentationTest("package t;\npublic class T {\n|private final String s = \"\"\"\n\"\"\";|\n}\n", + "package t;\npublic class T {\n private final String s = \"\"\"\n \"\"\";\n}\n"); + } + + public void testSpanIndentationTextBlock2() throws Exception { + try { + SourceVersion.valueOf("RELEASE_13"); + } catch (IllegalArgumentException ex) { + //OK, skip test: + return ; + } + performSpanIndentationTest("package t;\npublic class T {\n|private final String s = \"\"\"\n1\n 2\n 3\n\"\"\";|\n}\n", + "package t;\npublic class T {\n private final String s = \"\"\"\n 1\n 2\n 3\n \"\"\";\n}\n"); + } + private void performNewLineIndentationTest(String code, String golden) throws Exception { int pos = code.indexOf('|'); --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@netbeans.apache.org For additional commands, e-mail: commits-h...@netbeans.apache.org For further information about the NetBeans mailing lists, visit: https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists