diff --git a/java/java-impl/src/com/intellij/codeInsight/javadoc/JavaDocInfoGenerator.java b/java/java-impl/src/com/intellij/codeInsight/javadoc/JavaDocInfoGenerator.java index 458b92129b07..0d3c6bf94158 100644 --- a/java/java-impl/src/com/intellij/codeInsight/javadoc/JavaDocInfoGenerator.java +++ b/java/java-impl/src/com/intellij/codeInsight/javadoc/JavaDocInfoGenerator.java @@ -22,6 +22,7 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.DefaultLanguageHighlighterColors; import com.intellij.openapi.editor.HighlighterColors; import com.intellij.openapi.editor.colors.CodeInsightColors; +import com.intellij.openapi.editor.colors.EditorColors; import com.intellij.openapi.editor.colors.EditorColorsManager; import com.intellij.openapi.editor.colors.EditorColorsScheme; import com.intellij.openapi.editor.markup.TextAttributes; @@ -72,11 +73,15 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.*; +import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.function.UnaryOperator; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import static com.intellij.codeInsight.javadoc.SnippetMarkup.*; + public class JavaDocInfoGenerator { private static final Logger LOG = Logger.getInstance(JavaDocInfoGenerator.class); @@ -1817,7 +1822,7 @@ public class JavaDocInfoGenerator { List> files = InjectedLanguageManager.getInstance(snippetTag.getProject()).getInjectedPsiFiles(snippetTag); PsiElement element = files != null ? files.get(0).first : null; if (element != null && element.getLanguage().isKindOf(JavaLanguage.INSTANCE)) { - generateJavaSnippetBody(buffer, element); + generateJavaSnippetBody(buffer, element, e -> true); } else { for (PsiElement contentElement : body.getContent()) { @@ -1827,6 +1832,9 @@ public class JavaDocInfoGenerator { buffer.append(""); } else { PsiSnippetAttribute[] attributes = value.getAttributeList().getAttributes(); + PsiSnippetAttribute regionAttribute = ContainerUtil.find(attributes, attr -> attr.getName().equals(PsiSnippetAttribute.REGION_ATTRIBUTE)); + String region = regionAttribute == null || regionAttribute.getValue() == null ? null : + regionAttribute.getValue().getValue(); for (PsiSnippetAttribute attribute : attributes) { PsiSnippetAttributeValue attrValue = attribute.getValue(); if (attrValue != null) { @@ -1835,16 +1843,11 @@ public class JavaDocInfoGenerator { PsiElement resolved = ref.resolve(); if (resolved instanceof PsiFile file) { buffer.append("
");
-              if (file instanceof PsiJavaFile) {
-                generateJavaSnippetBody(buffer, file);
-              }
-              else {
-                buffer.append(file.getText());
-              }
+              generateSnippetBody(buffer, file, region);
               buffer.append("
"); } else { - buffer.append(getSpanForUnresolvedItem()).append(JavaBundle.message("javadoc.snippet.not.found")) - .append(attrValue.getValue()).append(""); + buffer.append(getSpanForUnresolvedItem()).append(JavaBundle.message("javadoc.snippet.not.found", attrValue.getValue())) + .append(""); } } } @@ -1852,20 +1855,84 @@ public class JavaDocInfoGenerator { } } - private void generateJavaSnippetBody(@NotNull StringBuilder buffer, PsiElement element) { + private void generateSnippetBody(@NotNull StringBuilder buffer, @NotNull PsiFile file, @Nullable String region) { + SnippetMarkup markup = forFile(file); + if (!markup.hasMarkup(region)) { + TextRange range = markup.getRegionRange(region); + if (range == null) { + buffer.append(getSpanForUnresolvedItem()).append(JavaBundle.message("javadoc.snippet.region.not.found", region)) + .append(""); + } else if (file instanceof PsiJavaFile) { + // Normal Java highlighting is only for regions without markup + generateJavaSnippetBody(buffer, file, + e -> { + TextRange textRange = e.getTextRange(); + return range.intersects(textRange) && markup.isTextPart(textRange); + }); + } else { + buffer.append(markup.getTextWithoutMarkup(region)); + } + return; + } + markup.visitSnippet(region, new SnippetVisitor() { + @Override + public void visitPlainText(@NotNull PlainText plainText, + @NotNull List<@NotNull LocationMarkupNode> activeNodes) { + String content = plainText.content(); + for (LocationMarkupNode node : activeNodes) { + UnaryOperator replacement; + if (node instanceof Replace replace) { + replacement = orig -> replace.replacement(); + } + else if (node instanceof Highlight highlight) { + replacement = switch (highlight.type()) { + case BOLD -> orig -> ""+orig+""; + case ITALIC -> orig -> ""+orig+""; + case HIGHLIGHTED -> { + TextAttributes attributes = + EditorColorsManager.getInstance().getGlobalScheme().getAttributes(EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES); + yield orig -> getStyledSpan(true, attributes, orig); + } + }; + } + else if (node instanceof Link link) { + replacement = orig -> { + StringBuilder buffer = new StringBuilder(); + DocumentationManagerUtil.createHyperlink(buffer, link.target(), orig, link.linkType() == LinkType.LINKPLAIN); + return buffer.toString(); + }; + } + else { + throw new AssertionError(node.toString()); + } + if (node.regex() != null) { + content = content.replaceAll(node.regex(), replacement.apply("$0")); + } + else if (node.substring() != null) { + content = content.replace(node.substring(), replacement.apply(node.substring())); + } + else { + content = replacement.apply(content); + } + } + buffer.append(content); + } + + @Override + public void visitError(@NotNull ErrorMarkup errorMarkup) { + buffer.append(getSpanForUnresolvedItem()).append("[").append(errorMarkup.message()).append("]"); + } + }); + } + + private void generateJavaSnippetBody(@NotNull StringBuilder buffer, @NotNull PsiElement element, @NotNull Predicate filter) { PsiFile containingFile = element.getContainingFile(); SyntaxTraverser.psiTraverser(containingFile) .filter(e -> e.getFirstChild() == null) + .filter(e -> e.getTextLength() > 0) + .filter(filter::test) .forEach(e -> { String text = e.getText(); - if (text.isEmpty()) return; - if (e instanceof PsiComment && text.startsWith("//")) { //todo: ignore markup as in JDK now - int idx = text.lastIndexOf("// @"); - if (idx >= 0) { - buffer.append(text, 0, idx); - return; - } - } JavaDocHighlightingManager manager = getHighlightingManager(); if (e instanceof PsiIdentifier) { PsiElement parent = e.getParent(); diff --git a/java/java-impl/src/com/intellij/codeInsight/javadoc/SnippetMarkup.java b/java/java-impl/src/com/intellij/codeInsight/javadoc/SnippetMarkup.java index eaae22966b3b..98652b5185dc 100644 --- a/java/java-impl/src/com/intellij/codeInsight/javadoc/SnippetMarkup.java +++ b/java/java-impl/src/com/intellij/codeInsight/javadoc/SnippetMarkup.java @@ -6,10 +6,14 @@ import com.intellij.openapi.util.NlsContexts; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; import com.intellij.psi.javadoc.PsiSnippetDocTagBody; +import com.intellij.psi.util.CachedValueProvider; +import com.intellij.psi.util.CachedValuesManager; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.Stack; import one.util.streamex.StreamEx; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -21,11 +25,13 @@ import java.util.stream.Stream; public class SnippetMarkup { private final @NotNull List<@NotNull MarkupNode> myNodes; + private final @NotNull Map<@NotNull String, @NotNull MarkupNode> myRegionStarts; + private final @NotNull BitSet myTextOffsets = new BitSet(); // \\S+ = language-dependent comment start token, like "//" or "#" private static final Pattern MARKUP_TAG = Pattern.compile("\\S+\\s*@(start|end|highlight|replace|link)\\s*(\\S.+?)?\\s*(:?)\\s*$"); - private static final Pattern ATTRIBUTE = Pattern.compile("(\\w+)\\s*(=\\s*(['\"]?)(\\S+)\\3)?"); + private static final Pattern ATTRIBUTE = Pattern.compile("(\\w+)\\s*(=\\s*('([^']*)'|\"([^\"]*)\"|(\\S*)))?"); private static final Map> ALLOWED_ATTRS = Map.of( @@ -38,6 +44,70 @@ public class SnippetMarkup { private SnippetMarkup(@NotNull List<@NotNull MarkupNode> nodes) { myNodes = nodes; + myRegionStarts = StreamEx.of(nodes) + .mapToEntry(n -> n instanceof StartRegion start ? start.region() : + n instanceof LocationMarkupNode node ? node.region() : + null, Function.identity()) + .removeKeys(k -> k == null || k.isEmpty()) + .distinctKeys() + .toMap(); + StreamEx.of(nodes) + .select(PlainText.class) + .forEach(text -> myTextOffsets.set(text.range().getStartOffset(), text.range().getEndOffset())); + } + + /** + * @param region region to test; null for the whole snippet + * @return true if any of {@code @replacement}, {@code @highlight}, or {@code @link} tags exist within the region + */ + public boolean hasMarkup(@Nullable String region) { + var visitor = new SnippetVisitor() { + boolean hasMarkup = false; + + @Override + public void visitPlainText(@NotNull PlainText plainText, @NotNull List<@NotNull LocationMarkupNode> activeNodes) { + hasMarkup |= !activeNodes.isEmpty(); + } + }; + visitSnippet(region, visitor); + return visitor.hasMarkup; + } + + public @Nullable TextRange getRegionRange(@Nullable String region) { + var visitor = new SnippetVisitor() { + TextRange myRange = null; + + @Override + public void visitPlainText(@NotNull PlainText plainText, @NotNull List<@NotNull LocationMarkupNode> activeNodes) { + myRange = myRange == null ? plainText.range() : myRange.union(plainText.range()); + } + }; + visitSnippet(region, visitor); + return visitor.myRange; + } + + /** + * @param range range to test + * @return true if the range completely or partially contains plain text chunk(s) to display + */ + public boolean isTextPart(@NotNull TextRange range) { + int nextSetBit = myTextOffsets.nextSetBit(range.getStartOffset()); + return nextSetBit != -1 && nextSetBit < range.getEndOffset(); + } + + /** + * @param region region to extract; null for the whole snippet + * @return text of a given region, ignoring markup + */ + public String getTextWithoutMarkup(@Nullable String region) { + StringBuilder result = new StringBuilder(); + visitSnippet(region, new SnippetVisitor() { + @Override + public void visitPlainText(@NotNull PlainText plainText, @NotNull List<@NotNull LocationMarkupNode> activeNodes) { + result.append(plainText.content()); + } + }); + return result.toString(); } /** @@ -63,6 +133,7 @@ public class SnippetMarkup { /** * @return range in the parent PSI element that corresponds to this element */ + @Contract(pure = true) @NotNull TextRange range(); } @@ -73,17 +144,20 @@ public class SnippetMarkup { /** * @return substring text to which this node is applicable */ + @Contract(pure = true) @Nullable String substring(); /** * @return regular expression, so this node is applicable to the matching substrings */ + @Contract(pure = true) @Nullable String regex(); /** * @return if present, defines a region name to which this tag is applicable. If null, then the tag is applicable to the next * {@link PlainText} node. */ + @Contract(pure = true) @Nullable String region(); } @@ -93,21 +167,6 @@ public class SnippetMarkup { * @param content text content (excluding the final linebreak) */ public record PlainText(@NotNull TextRange range, @NotNull String content) implements MarkupNode { - public PlainText { - if (range.getLength() != content.length()) { - throw new IllegalArgumentException(); - } - } - - /** - * @param start start offset of substring - * @param end end offset of substring - * @return PlainText object that corresponds to the substring of this object - */ - @NotNull PlainText substring(@SuppressWarnings("SameParameterValue") int start, int end) { - int offset = range().getStartOffset(); - return new PlainText(TextRange.create(offset + start, offset + end), content.substring(start, end)); - } } private record Attribute(@NotNull TextRange range, @NotNull String key, @Nullable String value) implements MarkupNode { @@ -167,6 +226,10 @@ public class SnippetMarkup { public static @NotNull SnippetMarkup parse(@NotNull String text) { return parse(preparse(text)); } + + public static @NotNull SnippetMarkup forFile(@NotNull PsiFile file) { + return CachedValuesManager.getCachedValue(file, () -> new CachedValueProvider.Result<>(parse(file.getText()), file)); + } public static @NotNull SnippetMarkup parse(@NotNull PsiSnippetDocTagBody body) { return parse(preparse(body)); @@ -182,7 +245,7 @@ public class SnippetMarkup { break; } else { - output.add(new PlainText(TextRange.create(pos, nextPos), text.substring(pos, nextPos))); + output.add(new PlainText(TextRange.create(pos, nextPos + 1), text.substring(pos, nextPos + 1))); pos = nextPos + 1; } } @@ -206,13 +269,14 @@ public class SnippetMarkup { Matcher matcher = MARKUP_TAG.matcher(content); if (!matcher.find()) return Stream.of(text); int start = matcher.start(); - PlainText prev = text.substring(0, start); + int offset = text.range().getStartOffset(); + PlainText prev = new PlainText(TextRange.create(offset, offset + start), text.content.substring(0, start) + "\n"); String tagName = matcher.group(1); List attrs = parseAttributes(text.range().getStartOffset() + matcher.start(2), matcher.group(2), ALLOWED_ATTRS.get(tagName)); List errors = new ArrayList<>(ContainerUtil.filterIsInstance(attrs, ErrorMarkup.class)); Map attrValues = StreamEx.of(attrs).select(Attribute.class).toMap(Attribute::key, Attribute::value); - TextRange range = TextRange.create(matcher.start(1), matcher.end()).shiftRight(text.range().getStartOffset()); + TextRange range = TextRange.create(matcher.start(1), matcher.end()).shiftRight(offset); boolean hasColon = !matcher.group(3).isEmpty(); MarkupNode node = switch (tagName) { case "start" -> { @@ -302,7 +366,11 @@ public class SnippetMarkup { result.add(new ErrorMarkup(range, JavaBundle.message("javadoc.snippet.error.duplicate.attribute", key))); } else { - result.add(new Attribute(range, key, Objects.requireNonNullElse(matcher.group(4), ""))); + String value = matcher.group(4); + if (value == null) value = matcher.group(5); + if (value == null) value = matcher.group(6); + if (value == null) value = ""; + result.add(new Attribute(range, key, value)); } pos = matcher.end(); } @@ -310,11 +378,26 @@ public class SnippetMarkup { } public interface SnippetVisitor { + /** + * Called for every plain text chunk + * @param plainText text chunk + * @param activeNodes active markup nodes to apply to this chunk + */ void visitPlainText(@NotNull PlainText plainText, @NotNull List<@NotNull LocationMarkupNode> activeNodes); - void visitError(@NotNull ErrorMarkup errorMarkup); + /** + * Called for every markup error visited + * @param errorMarkup error information + */ + default void visitError(@NotNull ErrorMarkup errorMarkup) {} } + /** + * Visit elements of the snippet within given region + * + * @param region region to visit + * @param visitor visitor to use + */ public void visitSnippet(@Nullable String region, SnippetVisitor visitor) { Stack regions = new Stack<>(); Map> active = new LinkedHashMap<>(); diff --git a/java/java-psi-api/src/com/intellij/psi/javadoc/PsiSnippetAttribute.java b/java/java-psi-api/src/com/intellij/psi/javadoc/PsiSnippetAttribute.java index fa4990df25f1..8a74b2b73721 100644 --- a/java/java-psi-api/src/com/intellij/psi/javadoc/PsiSnippetAttribute.java +++ b/java/java-psi-api/src/com/intellij/psi/javadoc/PsiSnippetAttribute.java @@ -15,6 +15,7 @@ import org.jetbrains.annotations.Nullable; public interface PsiSnippetAttribute extends PsiElement { String CLASS_ATTRIBUTE = "class"; String FILE_ATTRIBUTE = "file"; + String REGION_ATTRIBUTE = "region"; String SNIPPETS_FOLDER = "snippet-files"; PsiSnippetAttribute[] EMPTY_ARRAY = new PsiSnippetAttribute[0]; diff --git a/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/NoRegion.html b/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/NoRegion.html new file mode 100644 index 000000000000..fcf091041cb8 --- /dev/null +++ b/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/NoRegion.html @@ -0,0 +1,3 @@ +
class NoRegion
+
Region not found: noregion
+

\ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/NoRegion.java b/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/NoRegion.java new file mode 100644 index 000000000000..a2050cba686a --- /dev/null +++ b/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/NoRegion.java @@ -0,0 +1,5 @@ +/** + * {@snippet class=Region region=noregion} + */ +class NoRegion { +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/Region.html b/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/Region.html new file mode 100644 index 000000000000..98a6bfaf2afc --- /dev/null +++ b/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/Region.html @@ -0,0 +1,7 @@ +
class Region
+
  public static void main(String[] args) {
+    ... 
+    System.out.println("Hello"); 
+  }
+
+

\ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/Region.java b/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/Region.java new file mode 100644 index 000000000000..7069cae13074 --- /dev/null +++ b/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/Region.java @@ -0,0 +1,5 @@ +/** + * {@snippet class=Region region=main} + */ +class Region { +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/RegionNoMarkup.html b/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/RegionNoMarkup.html new file mode 100644 index 000000000000..a2549d69a02e --- /dev/null +++ b/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/RegionNoMarkup.html @@ -0,0 +1,7 @@ +
class RegionNoMarkup
+
+  public void test() {
+    System.out.println("Region no markup");
+  }
+  
+

\ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/RegionNoMarkup.java b/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/RegionNoMarkup.java new file mode 100644 index 000000000000..9a941cdaa092 --- /dev/null +++ b/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/RegionNoMarkup.java @@ -0,0 +1,5 @@ +/** + * {@snippet class=Region region=test} + */ +class RegionNoMarkup { +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/snippet-files/Region.java b/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/snippet-files/Region.java new file mode 100644 index 000000000000..358fe9413aba --- /dev/null +++ b/java/java-tests/testData/codeInsight/javadocIG/externalSnippet/snippet-files/Region.java @@ -0,0 +1,14 @@ +public class Region { + // @start region=main + public static void main(String[] args) { + code: // @replace regex=code: replacement="..." + System.out.println("Hello"); // @highlight substring=Hello + } + // @end + + // @start region=test + public void test() { + System.out.println("Region no markup"); + } + // @end region=test +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/javadocIG/inlineTagSnippet.html b/java/java-tests/testData/codeInsight/javadocIG/inlineTagSnippet.html index ac99f7ca2475..fdc626162322 100644 --- a/java/java-tests/testData/codeInsight/javadocIG/inlineTagSnippet.html +++ b/java/java-tests/testData/codeInsight/javadocIG/inlineTagSnippet.html @@ -1,5 +1,5 @@
class Main
Attributes: -
System.out.println("Hello, world!"); 
-int i = 0; //empty line 
+
System.out.println("Hello, world!"); // @link substring=System target=java.lang.System
+int i = 0; //empty line // @highlight substring=0

\ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/JavaDocInfoGeneratorTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/JavaDocInfoGeneratorTest.java index 67d88527fbc8..7cb93460ecc2 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/JavaDocInfoGeneratorTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/JavaDocInfoGeneratorTest.java @@ -128,6 +128,9 @@ public class JavaDocInfoGeneratorTest extends JavaCodeInsightTestCase { public void testInlineTagSnippetWithoutBody() { doTestClass(); } public void testExternalSnippet() { createProjectStructure(getTestDataPath() + TEST_DATA_FOLDER + "externalSnippet"); + verifyJavadocFor("Region"); + verifyJavadocFor("RegionNoMarkup"); + verifyJavadocFor("NoRegion"); verifyJavadocFor("Main"); verifyJavadocFor("TextFile"); verifyJavadocFor("Unresolved"); diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/SnippetMarkupTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/SnippetMarkupTest.java index a674f78d95c3..49b73b28210d 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/SnippetMarkupTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/javadoc/SnippetMarkupTest.java @@ -20,7 +20,8 @@ public class SnippetMarkupTest { """ Hello!!! World!!!""", """ - PlainText[range=(0,8), content=Hello!!!] + PlainText[range=(0,9), content=Hello!!! + ] PlainText[range=(9,17), content=World!!!]"""); } @@ -42,21 +43,29 @@ public class SnippetMarkupTest { // @end } """, """ - PlainText[range=(0,16), content=public class X {] - StartRegion[range=(23,40), region=test] - PlainText[range=(41,56), content= void main() {] - Replace[range=(94,133), substring=Hello, regex=null, region=null, replacement=...] - PlainText[range=(57,90), content= System.out.println("Hello"); ] - Highlight[range=(142,169), substring=Hello, regex=null, region=null, type=HIGHLIGHTED] - PlainText[range=(170,202), content= System.out.println("Hello");] - Highlight[range=(211,228), substring=null, regex=null, region=, type=HIGHLIGHTED] - PlainText[range=(229,262), content= System.out.println("Hello1");] - Link[range=(301,344), substring=null, regex=H\\w+, region=null, target=Hello, linkType=LINKPLAIN] - PlainText[range=(263,297), content= System.out.println("Hello2"); ] - EndRegion[range=(353,356), region=null] - PlainText[range=(357,360), content= }] - EndRegion[range=(367,370), region=null] - PlainText[range=(371,372), content=}] + PlainText[range=(0,17), content=public class X { + ] + StartRegion[range=(23,41), region=test] + PlainText[range=(41,57), content= void main() { + ] + Replace[range=(94,134), substring=Hello, regex=null, region=null, replacement=...] + PlainText[range=(57,90), content= System.out.println("Hello");\s + ] + Highlight[range=(142,170), substring=Hello, regex=null, region=null, type=HIGHLIGHTED] + PlainText[range=(170,203), content= System.out.println("Hello"); + ] + Highlight[range=(211,229), substring=null, regex=null, region=, type=HIGHLIGHTED] + PlainText[range=(229,263), content= System.out.println("Hello1"); + ] + Link[range=(301,345), substring=null, regex=H\\w+, region=null, target=Hello, linkType=LINKPLAIN] + PlainText[range=(263,297), content= System.out.println("Hello2");\s + ] + EndRegion[range=(353,357), region=null] + PlainText[range=(357,361), content= } + ] + EndRegion[range=(367,371), region=null] + PlainText[range=(371,373), content=} + ] PlainText[range=(373,373), content=]"""); } @@ -74,23 +83,29 @@ public class SnippetMarkupTest { // @end test } """, """ - PlainText[range=(0,16), content=public class X {] - ErrorMarkup[range=(23,28), message=@start: missing 'region' attribute] - PlainText[range=(29,44), content= void main() {] - Replace[range=(82,128), substring=Hello, regex=null, region=null, replacement=] + PlainText[range=(0,17), content=public class X { + ] + ErrorMarkup[range=(23,29), message=@start: missing 'region' attribute] + PlainText[range=(29,45), content= void main() { + ] + Replace[range=(82,129), substring=Hello, regex=null, region=null, replacement=] ErrorMarkup[range=(108,128), message=Unsupported attribute: 'substring2'] - ErrorMarkup[range=(82,128), message=@replace: missing 'replacement' attribute] - PlainText[range=(45,78), content= System.out.println("Hello"); ] - Highlight[range=(137,161), substring=null, regex=null, region=null, type=HIGHLIGHTED] + ErrorMarkup[range=(82,129), message=@replace: missing 'replacement' attribute] + PlainText[range=(45,78), content= System.out.println("Hello");\s + ] + Highlight[range=(137,162), substring=null, regex=null, region=null, type=HIGHLIGHTED] ErrorMarkup[range=(147,161), message=Unknown type 'underline'; only 'bold', 'italic', and 'highlighted' are supported] - Link[range=(199,213), substring=null, regex=null, region=null, target=, linkType=LINK] - ErrorMarkup[range=(199,213), message=@link: missing 'target' attribute] + Link[range=(199,214), substring=null, regex=null, region=null, target=, linkType=LINK] + ErrorMarkup[range=(199,214), message=@link: missing 'target' attribute] ErrorMarkup[range=(204,213), message=Unknown type 'pfff'; only 'link' and 'linkplain' are supported] - PlainText[range=(162,195), content= System.out.println("Hello"); ] - PlainText[range=(214,217), content= }] - EndRegion[range=(224,232), region=null] + PlainText[range=(162,195), content= System.out.println("Hello");\s + ] + PlainText[range=(214,218), content= } + ] + EndRegion[range=(224,233), region=null] ErrorMarkup[range=(228,232), message=Unsupported attribute: 'test'] - PlainText[range=(233,234), content=}] + PlainText[range=(233,235), content=} + ] PlainText[range=(235,235), content=]"""); } @@ -100,9 +115,9 @@ public class SnippetMarkupTest { public class Hello { // @start region=main public static void main(String[] args) { - code: // @replace regex='code:' replacement="..." - // @highlight substring="Hello" - System.out.println("Hello"); + code: // @replace regex='code:' replacement="" + // @highlight substring="Hello World" + System.out.println("Hello World"); // @highlight region substring="idx" for(int idx=0; idx<10; idx++) { System.out.println(idx); @@ -113,43 +128,71 @@ public class SnippetMarkupTest { } """; testParsing(text, """ - PlainText[range=(0,20), content=public class Hello {] - StartRegion[range=(27,44), region=main] - PlainText[range=(45,87), content= public static void main(String[] args) {] - Replace[range=(102,141), substring=null, regex=code:, region=null, replacement=...] - PlainText[range=(88,98), content= code: ] - Highlight[range=(150,177), substring=Hello, regex=null, region=null, type=HIGHLIGHTED] - PlainText[range=(178,210), content= System.out.println("Hello");] - Highlight[range=(219,251), substring=idx, regex=null, region=, type=HIGHLIGHTED] - PlainText[range=(252,287), content= for(int idx=0; idx<10; idx++) {] - PlainText[range=(288,318), content= System.out.println(idx);] - PlainText[range=(319,324), content= }] - EndRegion[range=(333,336), region=null] - PlainText[range=(337,340), content= }] - EndRegion[range=(347,350), region=null] - PlainText[range=(351,352), content=}] - PlainText[range=(353,353), content=]"""); + PlainText[range=(0,21), content=public class Hello { + ] + StartRegion[range=(27,45), region=main] + PlainText[range=(45,88), content= public static void main(String[] args) { + ] + Replace[range=(102,139), substring=null, regex=code:, region=null, replacement=] + PlainText[range=(88,98), content= code:\s + ] + Highlight[range=(147,181), substring=Hello World, regex=null, region=null, type=HIGHLIGHTED] + PlainText[range=(181,220), content= System.out.println("Hello World"); + ] + Highlight[range=(228,261), substring=idx, regex=null, region=, type=HIGHLIGHTED] + PlainText[range=(261,297), content= for(int idx=0; idx<10; idx++) { + ] + PlainText[range=(297,328), content= System.out.println(idx); + ] + PlainText[range=(328,334), content= } + ] + EndRegion[range=(342,346), region=null] + PlainText[range=(346,350), content= } + ] + EndRegion[range=(356,360), region=null] + PlainText[range=(360,362), content=} + ] + PlainText[range=(362,362), content=]"""); testVisitor(text, null, """ public class Hello { public static void main(String[] args) { - code: // [Replace[range=(102,141), substring=null, regex=code:, region=null, replacement=...]] - System.out.println("Hello"); // [Highlight[range=(150,177), substring=Hello, regex=null, region=null, type=HIGHLIGHTED]] - for(int idx=0; idx<10; idx++) { // [Highlight[range=(219,251), substring=idx, regex=null, region=, type=HIGHLIGHTED]] - System.out.println(idx); // [Highlight[range=(219,251), substring=idx, regex=null, region=, type=HIGHLIGHTED]] - } // [Highlight[range=(219,251), substring=idx, regex=null, region=, type=HIGHLIGHTED]] + // [Replace[range=(102,139), substring=null, regex=code:, region=null, replacement=]] + code:\s + // [Highlight[range=(147,181), substring=Hello World, regex=null, region=null, type=HIGHLIGHTED]] + System.out.println("Hello World"); + // [Highlight[range=(228,261), substring=idx, regex=null, region=, type=HIGHLIGHTED]] + for(int idx=0; idx<10; idx++) { + // [Highlight[range=(228,261), substring=idx, regex=null, region=, type=HIGHLIGHTED]] + System.out.println(idx); + // [Highlight[range=(228,261), substring=idx, regex=null, region=, type=HIGHLIGHTED]] + } } } - """); testVisitor(text, "main", """ public static void main(String[] args) { - code: // [Replace[range=(102,141), substring=null, regex=code:, region=null, replacement=...]] - System.out.println("Hello"); // [Highlight[range=(150,177), substring=Hello, regex=null, region=null, type=HIGHLIGHTED]] - for(int idx=0; idx<10; idx++) { // [Highlight[range=(219,251), substring=idx, regex=null, region=, type=HIGHLIGHTED]] - System.out.println(idx); // [Highlight[range=(219,251), substring=idx, regex=null, region=, type=HIGHLIGHTED]] - } // [Highlight[range=(219,251), substring=idx, regex=null, region=, type=HIGHLIGHTED]] + // [Replace[range=(102,139), substring=null, regex=code:, region=null, replacement=]] + code:\s + // [Highlight[range=(147,181), substring=Hello World, regex=null, region=null, type=HIGHLIGHTED]] + System.out.println("Hello World"); + // [Highlight[range=(228,261), substring=idx, regex=null, region=, type=HIGHLIGHTED]] + for(int idx=0; idx<10; idx++) { + // [Highlight[range=(228,261), substring=idx, regex=null, region=, type=HIGHLIGHTED]] + System.out.println(idx); + // [Highlight[range=(228,261), substring=idx, regex=null, region=, type=HIGHLIGHTED]] + } } """); + assertEquals(""" + public static void main(String[] args) { + code:\s + System.out.println("Hello World"); + for(int idx=0; idx<10; idx++) { + System.out.println(idx); + } + } + """, SnippetMarkup.parse(text).getTextWithoutMarkup("main")); + } @Test @@ -167,36 +210,53 @@ public class SnippetMarkupTest { // @end region="r2" """; testParsing(text, """ - Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD] - PlainText[range=(52,64), content=r1 start xxx] - Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC] - PlainText[range=(119,131), content=r2 start yyy] - Highlight[range=(136,179), substring=zzz, regex=null, region=null, type=HIGHLIGHTED] - PlainText[range=(180,210), content=one line with zzz highlighting] - PlainText[range=(211,220), content=r1+r2 xxx] - EndRegion[range=(225,240), region=r1] - PlainText[range=(241,257), content=r2 continues yyy] - EndRegion[range=(262,277), region=r2] + Highlight[range=(4,52), substring=xxx, regex=null, region=r1, type=BOLD] + PlainText[range=(52,65), content=r1 start xxx + ] + Highlight[range=(69,119), substring=yyy, regex=null, region=r2, type=ITALIC] + PlainText[range=(119,132), content=r2 start yyy + ] + Highlight[range=(136,180), substring=zzz, regex=null, region=null, type=HIGHLIGHTED] + PlainText[range=(180,211), content=one line with zzz highlighting + ] + PlainText[range=(211,221), content=r1+r2 xxx + ] + EndRegion[range=(225,241), region=r1] + PlainText[range=(241,258), content=r2 continues yyy + ] + EndRegion[range=(262,278), region=r2] PlainText[range=(278,278), content=]"""); testVisitor(text, null, """ - r1 start xxx // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD]] - r2 start yyy // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC]] - one line with zzz highlighting // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC], Highlight[range=(136,179), substring=zzz, regex=null, region=null, type=HIGHLIGHTED]] - r1+r2 xxx // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC]] - r2 continues yyy // [Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC]] - + // [Highlight[range=(4,52), substring=xxx, regex=null, region=r1, type=BOLD]] + r1 start xxx + // [Highlight[range=(4,52), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,119), substring=yyy, regex=null, region=r2, type=ITALIC]] + r2 start yyy + // [Highlight[range=(4,52), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,119), substring=yyy, regex=null, region=r2, type=ITALIC], Highlight[range=(136,180), substring=zzz, regex=null, region=null, type=HIGHLIGHTED]] + one line with zzz highlighting + // [Highlight[range=(4,52), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,119), substring=yyy, regex=null, region=r2, type=ITALIC]] + r1+r2 xxx + // [Highlight[range=(69,119), substring=yyy, regex=null, region=r2, type=ITALIC]] + r2 continues yyy """); testVisitor(text, "r1", """ - r1 start xxx // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD]] - r2 start yyy // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC]] - one line with zzz highlighting // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC], Highlight[range=(136,179), substring=zzz, regex=null, region=null, type=HIGHLIGHTED]] - r1+r2 xxx // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC]] + // [Highlight[range=(4,52), substring=xxx, regex=null, region=r1, type=BOLD]] + r1 start xxx + // [Highlight[range=(4,52), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,119), substring=yyy, regex=null, region=r2, type=ITALIC]] + r2 start yyy + // [Highlight[range=(4,52), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,119), substring=yyy, regex=null, region=r2, type=ITALIC], Highlight[range=(136,180), substring=zzz, regex=null, region=null, type=HIGHLIGHTED]] + one line with zzz highlighting + // [Highlight[range=(4,52), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,119), substring=yyy, regex=null, region=r2, type=ITALIC]] + r1+r2 xxx """); testVisitor(text, "r2", """ - r2 start yyy // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC]] - one line with zzz highlighting // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC], Highlight[range=(136,179), substring=zzz, regex=null, region=null, type=HIGHLIGHTED]] - r1+r2 xxx // [Highlight[range=(4,51), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC]] - r2 continues yyy // [Highlight[range=(69,118), substring=yyy, regex=null, region=r2, type=ITALIC]] + // [Highlight[range=(4,52), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,119), substring=yyy, regex=null, region=r2, type=ITALIC]] + r2 start yyy + // [Highlight[range=(4,52), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,119), substring=yyy, regex=null, region=r2, type=ITALIC], Highlight[range=(136,180), substring=zzz, regex=null, region=null, type=HIGHLIGHTED]] + one line with zzz highlighting + // [Highlight[range=(4,52), substring=xxx, regex=null, region=r1, type=BOLD], Highlight[range=(69,119), substring=yyy, regex=null, region=r2, type=ITALIC]] + r1+r2 xxx + // [Highlight[range=(69,119), substring=yyy, regex=null, region=r2, type=ITALIC]] + r2 continues yyy """); } @@ -211,11 +271,10 @@ public class SnippetMarkupTest { @Override public void visitPlainText(@NotNull PlainText plainText, @NotNull List<@NotNull LocationMarkupNode> activeNodes) { - sb.append(plainText.content()); if (!activeNodes.isEmpty()) { - sb.append(" // ").append(activeNodes); + sb.append("// ").append(activeNodes).append("\n"); } - sb.append("\n"); + sb.append(plainText.content()); } @Override diff --git a/java/openapi/resources/messages/JavaBundle.properties b/java/openapi/resources/messages/JavaBundle.properties index 1014b6b855e5..5579fda76148 100644 --- a/java/openapi/resources/messages/JavaBundle.properties +++ b/java/openapi/resources/messages/JavaBundle.properties @@ -1818,7 +1818,8 @@ intention.family.name.ignore.project=Do not show again in this project fix.move.to.source.root=Move to source root warning.java.file.outside.source.root=Java file is located outside of the module source root, so it won't be compiled inspection.message.snippet.file.not.found=Snippet file is not found -javadoc.snippet.not.found=Snippet not found: +javadoc.snippet.not.found=Snippet not found: {0} +javadoc.snippet.region.not.found=Region not found: {0} javadoc.snippet.error.unknown.highlight.type=Unknown type ''{0}''; only ''bold'', ''italic'', and ''highlighted'' are supported javadoc.snippet.error.unknown.link.type=Unknown type ''{0}''; only ''link'' and ''linkplain'' are supported javadoc.snippet.error.malformed.attribute=Malformed attribute