Files
openide/java/java-impl/src/com/intellij/codeInsight/javadoc/SnippetMarkup.java
T
Tagir Valeevandintellij-monorepo-bot fd093ea5ff [multiple] Cleanup: use record patterns
GitOrigin-RevId: dca054f83dadfb60c9aa1b0d950423fc55e3a7a3
2025-09-15 14:09:19 +00:00

762 lines
29 KiB
Java

// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.javadoc;
import com.intellij.ide.nls.NlsMessages;
import com.intellij.java.JavaBundle;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.impl.FakePsiElement;
import com.intellij.psi.javadoc.*;
import com.intellij.psi.tree.IElementType;
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;
import org.jetbrains.annotations.TestOnly;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
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 PsiElement myContext;
private final @NotNull BitSet myTextOffsets = new BitSet();
private static final Map<String, Set<String>> ALLOWED_ATTRS =
Map.of(
"start", Set.of("region"),
"end", Set.of("region"),
"highlight", Set.of("substring", "regex", "region", "type"),
"replace", Set.of("substring", "regex", "region", "replacement"),
"link", Set.of("substring", "regex", "region", "target", "type")
);
private static final String ALLOWED_TAGS = "start|end|highlight|replace|link";
private static final Pattern MARKUP_SPEC = Pattern.compile("(?://|#|rem|REM|')\\s*(@(?:" + ALLOWED_TAGS + ")(?:\\s.+)?)$");
private static final Pattern MARKUP_TAG = Pattern.compile("@(" + ALLOWED_TAGS + ")\\s*");
private static final Pattern ATTRIBUTE = Pattern.compile("(\\w+)\\s*(=\\s*('([^']*)'|\"([^\"]*)\"|(\\S*)))?\\s*");
private SnippetMarkup(@NotNull List<@NotNull MarkupNode> nodes, @NotNull PsiElement context) {
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()
.toImmutableMap();
myContext = context;
StreamEx.of(nodes)
.select(PlainText.class)
.forEach(text -> myTextOffsets.set(text.range().getStartOffset(), text.range().getEndOffset()));
}
public @NotNull PsiElement getContext() {
return myContext;
}
/**
* @param region region
* @return starting node of the region
*/
public @Nullable MarkupNode getRegionStart(@NotNull String region) {
return myRegionStarts.get(region);
}
/**
* @param region region to process; null for the whole snippet
* @return common indent of all the lines in the region
*/
public int getCommonIndent(@Nullable String region) {
var visitor = new SnippetVisitor() {
int maxIndent = Integer.MAX_VALUE;
@Override
public void visitPlainText(@NotNull PlainText plainText, @NotNull List<@NotNull LocationMarkupNode> activeNodes) {
if (maxIndent == 0) return;
String content = plainText.content();
if (content.isBlank()) return;
int curIndent = 0;
while (curIndent < content.length() && content.charAt(curIndent) != '\n' && Character.isWhitespace(content.charAt(curIndent))) {
curIndent++;
}
maxIndent = Math.min(maxIndent, curIndent);
}
};
visitSnippet(region, visitor);
return visitor.maxIndent == Integer.MAX_VALUE ? 0 : visitor.maxIndent;
}
/**
* @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();
}
public Set<String> getRegions() {
return myRegionStarts.keySet();
}
/**
* Type of the highlighting
* @see Highlight
*/
public enum HighlightType {
BOLD, ITALIC, HIGHLIGHTED
}
/**
* Type of the link
* @see Link
*/
public enum LinkType {
LINK, LINKPLAIN
}
/**
* Element of snippet text
*/
public sealed interface MarkupNode {
/**
* @return range in the parent PSI element that corresponds to this element
*/
@Contract(pure = true)
@NotNull TextRange range();
}
/**
* An interface that specifies to which part of text content the {@link LocationMarkupNode} is applied
*/
public sealed interface Selector {
/**
* @param string string to find the ranges
* @return non-overlapping sorted ranges inside the string matched by this selector
*/
@NotNull List<TextRange> ranges(String string);
}
/**
* Substring-selector; applicable to a specific substring
* @param substring substring to apply to
*/
public record Substring(@NotNull String substring) implements Selector {
@Override
public @NotNull List<TextRange> ranges(String string) {
int pos = 0;
List<TextRange> ranges = new ArrayList<>();
while (true) {
int nextPos = string.indexOf(substring, pos);
if (nextPos == -1) break;
ranges.add(TextRange.from(nextPos, substring.length()));
pos = nextPos + substring.length() + 1;
}
return ranges;
}
}
/**
* Regex-selector; applicable to a specific regex
* @param pattern pattern to apply to
*/
public record Regex(@NotNull Pattern pattern) implements Selector {
@Override
public @NotNull List<TextRange> ranges(String string) {
Matcher matcher = pattern.matcher(string);
List<TextRange> ranges = new ArrayList<>();
while (matcher.find()) {
ranges.add(TextRange.create(matcher.start(), matcher.end()));
}
return ranges;
}
}
/**
* Selector which is applicable to the whole line
*/
public record WholeLine() implements Selector {
@Override
public @NotNull List<TextRange> ranges(String string) {
return List.of(TextRange.create(0, string.endsWith("\n") ? string.length() - 1 : string.length()));
}
}
/**
* A markup tag applicable to a particular part of original text
*/
public sealed interface LocationMarkupNode extends MarkupNode {
/**
* @return selector to which this node is applicable
*/
@Contract(pure = true)
@NotNull Selector selector();
/**
* @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();
}
/**
* A node that represents a chunk of plain text, ending with a linebreak
*
* @param content text content (excluding the final linebreak)
*/
public record PlainText(@NotNull TextRange range, @NotNull String content) implements MarkupNode {
}
private record Attribute(@NotNull TextRange range, @NotNull String key, @NotNull String value) implements MarkupNode {
}
/**
* Represents a {@code @start} tag
*
* @param region name of the region
*/
public record StartRegion(@NotNull TextRange range, @NotNull String region) implements MarkupNode {
}
/**
* Represents an {@code @end} tag
* @param region name of the region to close. If null then the most recently open region is closed by this tag
*/
public record EndRegion(@NotNull TextRange range, @Nullable String region) implements MarkupNode {
}
/**
* Represents a malformed markup chunk
*
* @param message localized error message
*/
public record ErrorMarkup(@NotNull TextRange range, @NlsContexts.ParsingError @NotNull String message) implements MarkupNode {
}
/**
* Represents a {@code @highlight} tag
*
* @param type highlighting type
*/
public record Highlight(@NotNull TextRange range, @NotNull Selector selector, @Nullable String region,
@NotNull HighlightType type) implements LocationMarkupNode {
}
/**
* Represents a {@code @replace} tag
*
* @param replacement replacement text
*/
public record Replace(@NotNull TextRange range, @NotNull Selector selector, @Nullable String region,
@NotNull String replacement) implements LocationMarkupNode {
public Replace withReplacement(@NotNull String replacement) {
return replacement.equals(this.replacement) ? this : new Replace(range, selector, region, replacement);
}
}
/**
* Represents a {@code @link} tag
*
* @param target link target
* @param linkType link type
*/
public record Link(@NotNull TextRange range, @NotNull Selector selector, @Nullable String region, @NotNull String target,
@NotNull LinkType linkType) implements LocationMarkupNode {
}
@TestOnly
public static @NotNull SnippetMarkup parse(@NotNull String text) {
return parse(preparse(text), new FakePsiElement() {
@Override
public PsiElement getParent() {
return null;
}
});
}
/**
* @param element {@link PsiFile} or {@link PsiSnippetDocTagBody}
* @return markup for a given element
*/
public static @NotNull SnippetMarkup fromElement(@NotNull PsiElement element) {
if (!(element instanceof PsiFile) && !(element instanceof PsiSnippetDocTagBody)) {
throw new IllegalArgumentException();
}
return CachedValuesManager.getCachedValue(element, () -> {
SnippetMarkup markup = element instanceof PsiSnippetDocTagBody body ? parse(preparse(body), body) :
parse(preparse(element.getText()), element);
return new CachedValueProvider.Result<>(markup, element);
});
}
/**
* @param snippet snippet to create a markup for
* @return a markup from snippet. For hybrid snippet, markup is created from the external snippet.
* Returns null if there's no snippet body and no external snippet resolved
*/
public static @Nullable SnippetMarkup fromSnippet(@NotNull PsiSnippetDocTagValue snippet) {
SnippetMarkup markup = fromExternalSnippet(snippet);
if (markup != null) {
return markup;
}
PsiSnippetDocTagBody body = snippet.getBody();
return body != null ? fromElement(body) : null;
}
/**
* @param snippet snippet to create a markup for
* @return markup from external snippet (ignoring body), null if there's no external snippet resolved.
*/
public static @Nullable SnippetMarkup fromExternalSnippet(@NotNull PsiSnippetDocTagValue snippet) {
PsiSnippetAttributeList list = snippet.getAttributeList();
PsiSnippetAttribute refAttribute = list.getAttribute(PsiSnippetAttribute.CLASS_ATTRIBUTE);
if (refAttribute == null) {
refAttribute = list.getAttribute(PsiSnippetAttribute.FILE_ATTRIBUTE);
}
if (refAttribute == null) return null;
PsiSnippetAttributeValue attrValue = refAttribute.getValue();
if (attrValue == null) return null;
PsiReference ref = attrValue.getReference();
if (ref == null || !(ref.resolve() instanceof PsiFile file)) return null;
return fromElement(file);
}
private static @NotNull List<@NotNull PlainText> preparse(@NotNull String text) {
List<PlainText> output = new ArrayList<>();
int pos = 0;
while (true) {
int nextPos = text.indexOf('\n', pos);
if (nextPos == -1) {
output.add(new PlainText(TextRange.create(pos, text.length()), text.substring(pos)));
break;
}
else {
output.add(new PlainText(TextRange.create(pos, nextPos + 1), text.substring(pos, nextPos + 1)));
pos = nextPos + 1;
}
}
return output;
}
private static @NotNull List<@NotNull PlainText> preparse(@NotNull PsiSnippetDocTagBody body) {
List<PlainText> output = new ArrayList<>();
PsiElement[] children = body.getChildren();
boolean first = true;
for (PsiElement element : children) {
if (element instanceof PsiDocToken token) {
IElementType tokenType = token.getTokenType();
if (tokenType.equals(JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS)) continue;
if (tokenType.equals(JavaDocTokenType.DOC_COMMENT_DATA)) {
output.add(new PlainText(element.getTextRangeInParent(), element.getText()));
}
}
else if (element instanceof PsiWhiteSpace) {
String text = element.getText();
if (text.contains("\n")) {
int idx = output.size() - 1;
if (idx >= 0 && !output.get(idx).content().endsWith("\n")) {
output.set(idx, new PlainText(output.get(idx).range(), output.get(idx).content() + "\n"));
}
else if (first) {
first = false;
}
else {
output.add(new PlainText(element.getTextRange(), "\n"));
}
}
}
}
return output;
}
private static @NotNull SnippetMarkup parse(@NotNull List<@NotNull PlainText> preparsed, @NotNull PsiElement context) {
return new SnippetMarkup(preparsed.stream().flatMap(SnippetMarkup::parseLine).toList(), context);
}
private static Stream<MarkupNode> parseLine(@NotNull PlainText text) {
String content = text.content();
Matcher matcher = MARKUP_SPEC.matcher(content);
if (!matcher.find()) return Stream.of(text);
int offset = text.range().getStartOffset();
String markupSpec = matcher.group(1).stripTrailing();
boolean hasColon = markupSpec.endsWith(":");
if (hasColon) {
markupSpec = markupSpec.substring(0, markupSpec.length() - 1);
}
int start = matcher.start(0);
PlainText prev = new PlainText(TextRange.create(offset, offset + start), text.content.substring(0, start) + "\n");
start = matcher.start(1);
List<MarkupNode> markupNodes = new ArrayList<>();
while (!markupSpec.isEmpty()) {
Matcher tagMatcher = MARKUP_TAG.matcher(markupSpec);
if (!tagMatcher.find()) {
markupNodes.add(new ErrorMarkup(TextRange.create(0, markupSpec.length()).shiftRight(start + offset),
JavaBundle.message("javadoc.snippet.error.markup.tag.expected")));
break;
}
if (tagMatcher.start() != 0) {
markupNodes.add(new ErrorMarkup(TextRange.create(0, tagMatcher.start()).shiftRight(start + offset),
JavaBundle.message("javadoc.snippet.error.markup.tag.expected")));
break;
}
String tagName = tagMatcher.group(1);
Set<String> allowedAttrs = ALLOWED_ATTRS.get(tagName);
Map<String, Attribute> attrs = new HashMap<>();
int attrPos = tagMatcher.end();
int attrEnd = attrPos;
while (attrPos < markupSpec.length()) {
Matcher attrMatcher = ATTRIBUTE.matcher(markupSpec);
boolean found = attrMatcher.find(attrPos);
if (!found || attrMatcher.start() != attrPos) break;
String key = attrMatcher.group(1);
attrEnd = attrMatcher.end(2);
if (attrEnd == -1) {
attrEnd = attrMatcher.end(1);
}
TextRange attrRange = TextRange.create(attrMatcher.start(), attrEnd).shiftRight(start + offset);
if (!allowedAttrs.contains(key)) {
markupNodes.add(new ErrorMarkup(attrRange, JavaBundle.message("javadoc.snippet.error.unsupported.attribute", tagName, key)));
}
else if (attrs.containsKey(key)) {
markupNodes.add(new ErrorMarkup(attrRange, JavaBundle.message("javadoc.snippet.error.duplicate.attribute", tagName, key)));
}
else {
String value = attrMatcher.group(4);
if (value == null) value = attrMatcher.group(5);
if (value == null) value = attrMatcher.group(6);
if (value == null) value = "";
attrs.put(key, new Attribute(attrRange, key, value));
}
attrPos = attrMatcher.end();
}
while (attrEnd > 0 && Character.isWhitespace(markupSpec.charAt(attrEnd - 1))) {
attrEnd--;
}
TextRange range = TextRange.create(0, attrEnd).shiftRight(start + offset);
validateAndAddTag(markupNodes, tagName, attrs, range);
start += attrPos;
markupSpec = markupSpec.substring(attrPos);
}
if (prev.content().isBlank() && !prev.content().isEmpty()) {
prev = new PlainText(TextRange.from(text.range().getEndOffset(), 0), "");
}
if (hasColon || (!prev.content().isEmpty() && ContainerUtil.and(markupNodes, mn -> mn instanceof EndRegion))) {
markupNodes.add(0, prev);
}
else {
markupNodes.add(prev);
}
return markupNodes.stream();
}
private static void validateAndAddTag(@NotNull List<MarkupNode> markupNodes,
@NotNull String tagName,
@NotNull Map<String, Attribute> attrs,
@NotNull TextRange range) {
String region = getRegion(attrs);
switch (tagName) {
case "start" -> {
region = getRequiredString(markupNodes, attrs, tagName, range, "region");
if (region != null) {
markupNodes.add(new StartRegion(range, region));
}
}
case "end" -> markupNodes.add(new EndRegion(range, region));
case "highlight" -> {
Attribute typeAttr = attrs.get("type");
HighlightType type = getEnumValue(markupNodes, "highlight", typeAttr, HighlightType.class, HighlightType.HIGHLIGHTED);
Selector selector = getSelector(markupNodes, attrs, tagName);
if (selector != null) {
markupNodes.add(new Highlight(range, selector, region, type));
}
else if (region != null) {
markupNodes.add(new StartRegion(range, region));
}
}
case "replace" -> {
String replacement = getRequiredString(markupNodes, attrs, tagName, range, "replacement");
Selector selector = getSelector(markupNodes, attrs, tagName);
if (replacement != null && selector != null) {
markupNodes.add(new Replace(range, selector, region, replacement));
}
else if (region != null) {
markupNodes.add(new StartRegion(range, region));
}
}
case "link" -> {
String target = getRequiredString(markupNodes, attrs, tagName, range, "target");
Attribute typeAttr = attrs.get("type");
LinkType type = getEnumValue(markupNodes, "link", typeAttr, LinkType.class, LinkType.LINK);
Selector selector = getSelector(markupNodes, attrs, tagName);
if (target != null && selector != null) {
markupNodes.add(new Link(range, selector, region, target, type));
}
else if (region != null) {
markupNodes.add(new StartRegion(range, region));
}
}
default -> throw new AssertionError("Unexpected tag: " + tagName);
}
}
private static <T extends Enum<T>> @NotNull T getEnumValue(@NotNull List<MarkupNode> markupNodes, @NotNull String tagName,
@Nullable Attribute attribute, @NotNull Class<T> enumClass, @NotNull T defaultValue) {
if (attribute == null) return defaultValue;
T value = ContainerUtil.find(enumClass.getEnumConstants(), val -> val.name().toLowerCase(Locale.ROOT).equals(attribute.value()));
if (value == null) {
markupNodes.add(
new ErrorMarkup(attribute.range(), JavaBundle.message(
"javadoc.snippet.error.unknown.enum.value",
tagName, attribute.key(), attribute.value(),
StreamEx.of(enumClass.getEnumConstants()).map(val -> "'" + val.name().toLowerCase(Locale.ROOT) + "'")
.collect(NlsMessages.joiningAnd()))));
return defaultValue;
}
return value;
}
private static @Nullable String getRegion(@NotNull Map<String, Attribute> attrs) {
Attribute attr = attrs.get("region");
return attr == null ? null : attr.value();
}
private static @Nullable String getRequiredString(@NotNull List<MarkupNode> markupNodes, @NotNull Map<String, Attribute> attrs,
@NotNull String tagName, @NotNull TextRange range, @NotNull String attrName) {
Attribute attr = attrs.get(attrName);
if (attr == null) {
markupNodes.add(new ErrorMarkup(range, JavaBundle.message("javadoc.snippet.error.missing.required.attribute", tagName, attrName)));
return null;
}
return attr.value();
}
private static @Nullable Selector getSelector(@NotNull List<MarkupNode> markupNodes, @NotNull Map<String, Attribute> attrs,
@NotNull String tagName) {
Attribute substring = attrs.get("substring");
Attribute regex = attrs.get("regex");
if (substring == null && regex == null) return new WholeLine();
if (substring != null && regex != null) {
markupNodes.add(new ErrorMarkup(regex.range(), JavaBundle.message("javadoc.snippet.error.both.substring.and.regex", tagName)));
return null;
}
if (regex == null) return new Substring(substring.value());
try {
return new Regex(Pattern.compile(regex.value()));
}
catch (PatternSyntaxException e) {
markupNodes.add(new ErrorMarkup(regex.range(),
JavaBundle.message("javadoc.snippet.error.malformed.regular.expression", tagName, e.getMessage().replace("\r\n", "\n"))));
return null;
}
}
public interface SnippetVisitor {
/**
* Called for every plain text chunk
*
* @param plainText text chunk
* @param activeNodes active markup nodes to apply to this chunk
*/
default void visitPlainText(@NotNull PlainText plainText, @NotNull List<@NotNull LocationMarkupNode> activeNodes) { }
/**
* 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, @NotNull SnippetVisitor visitor) {
visitSnippet(region, false, visitor);
}
/**
* Visit elements of the snippet within given region
*
* @param region region to visit
* @param preprocess if true, {@code @replace} tags will be processed automatically and not passed to the visitor;
* also common indent will be stripped automatically from {@link PlainText} nodes. This option
* is useful if you want to actually render markup. In this case, you only have to process
* {@link Highlight} and {@link Link} tags.
* @param visitor visitor to use
*/
public void visitSnippet(@Nullable String region, boolean preprocess, @NotNull SnippetVisitor visitor) {
Stack<String> regions = new Stack<>();
Map<String, List<LocationMarkupNode>> active = new LinkedHashMap<>();
int commonIndent = preprocess ? getCommonIndent(region) : 0;
for (MarkupNode node : myNodes) {
if (node instanceof StartRegion start) {
regions.push(start.region());
}
else if (node instanceof EndRegion end) {
if (end.region() == null) {
if (!regions.isEmpty()) {
active.remove(regions.pop());
}
}
else {
regions.remove(end.region());
active.remove(end.region());
}
}
else if (node instanceof LocationMarkupNode loc) {
String regionName = loc.region();
if (regionName != null) {
regions.push(regionName);
}
active.computeIfAbsent(regionName, k -> new ArrayList<>()).add(loc);
}
else if (node instanceof PlainText plainText) {
if (region == null || regions.contains(region)) {
plainText = stripIndent(plainText, commonIndent);
List<LocationMarkupNode> flatActive = StreamEx.ofValues(active).toFlatList(Function.identity());
if (preprocess) {
processReplacements(visitor, plainText, flatActive);
}
else {
visitor.visitPlainText(plainText, flatActive);
}
}
active.values().forEach(
list -> list.replaceAll(
n -> n instanceof Replace repl && repl.selector() instanceof WholeLine ? repl.withReplacement("") : n));
active.remove(null);
}
else if (node instanceof ErrorMarkup errorMarkup) {
if (region == null || regions.contains(region)) {
visitor.visitError(errorMarkup);
}
}
}
}
private static PlainText stripIndent(PlainText plainText, int commonIndent) {
if (commonIndent <= 0) return plainText;
String content = plainText.content();
int curIndent = 0;
while (curIndent < commonIndent &&
curIndent < content.length() &&
content.charAt(curIndent) != '\n' &&
Character.isWhitespace(content.charAt(curIndent))) {
curIndent++;
}
if (curIndent == 0) return plainText;
content = content.substring(curIndent);
return new PlainText(TextRange.create(plainText.range().getStartOffset() + curIndent, plainText.range().getEndOffset()), content);
}
private static void processReplacements(@NotNull SnippetVisitor visitor, @NotNull PlainText plainText, @NotNull List<LocationMarkupNode> flatActive) {
Map<Boolean, List<LocationMarkupNode>> map = StreamEx.of(flatActive).partitioningBy(n -> n instanceof Replace);
flatActive = map.get(false);
String content = plainText.content();
for (LocationMarkupNode markupNode : map.get(true)) {
Replace replace = (Replace)markupNode;
Selector selector = replace.selector();
String replacement = replace.replacement();
if (selector instanceof Regex(Pattern pattern)) {
boolean addLineBreak = false;
if (content.endsWith("\n")) {
content = content.substring(0, content.length() - 1);
addLineBreak = true;
}
try {
content = pattern.matcher(StringUtil.newBombedCharSequence(content, 1000)).replaceAll(replacement);
}
catch (StackOverflowError | ProcessCanceledException e) {
ErrorMarkup replacementError = new ErrorMarkup(
replace.range(), JavaBundle.message("javadoc.snippet.error.regex.too.complex", "replace", pattern.pattern()));
visitor.visitError(replacementError);
}
catch (IllegalArgumentException | IndexOutOfBoundsException e) {
ErrorMarkup replacementError = new ErrorMarkup(
replace.range(), JavaBundle.message("javadoc.snippet.error.malformed.replacement", "replace", replacement, e.getMessage()));
visitor.visitError(replacementError);
}
if (addLineBreak) {
content += "\n";
}
}
else if (selector instanceof Substring(String substring)) {
content = content.replace(substring, replacement);
}
else {
content = replacement;
}
}
visitor.visitPlainText(new PlainText(plainText.range(), content), flatActive);
}
@Override
public String toString() {
return StringUtil.join(myNodes, "\n");
}
}