WEB-32876 WEB-36410 Support formatting of multiple injections within single XmlText element.

GitOrigin-RevId: 217283985c2e6d210f04c4f1e9f8d950181ee81e
This commit is contained in:
Piotr Tomiak
2020-05-28 17:54:10 +03:00
committed by intellij-monorepo-bot
parent d5c826aaa2
commit dd4e91f493
7 changed files with 211 additions and 67 deletions
@@ -55,24 +55,20 @@ public abstract class InjectedLanguageBlockBuilder {
public abstract Block createBlockAfterInjection(ASTNode node, Wrap wrap, Alignment alignment, Indent indent, TextRange range);
public boolean addInjectedBlocks(List<? super Block> result, final ASTNode injectionHost, Wrap wrap, Alignment alignment, Indent indent) {
final PsiFile[] injectedFile = new PsiFile[1];
final Ref<TextRange> injectedRangeInsideHost = new Ref<>();
final Ref<Integer> prefixLength = new Ref<>();
final Ref<Integer> suffixLength = new Ref<>();
final Ref<ASTNode> injectionHostToUse = new Ref<>(injectionHost);
final Ref<Integer> lastInjectionEndOffset = new Ref<>(0);
final PsiLanguageInjectionHost.InjectedPsiVisitor injectedPsiVisitor = (injectedPsi, places) -> {
if (places.size() != 1) {
return;
}
final PsiLanguageInjectionHost.Shred shred = places.get(0);
TextRange textRange = shred.getRangeInsideHost();
TextRange injectionRange = shred.getRangeInsideHost();
PsiLanguageInjectionHost shredHost = shred.getHost();
if (shredHost == null) {
return;
}
ASTNode node = shredHost.getNode();
if (node == null || !injectionHost.getTextRange().contains(textRange.shiftRight(node.getStartOffset()))) {
if (node == null || !injectionHost.getTextRange().contains(injectionRange.shiftRight(node.getStartOffset()))) {
return;
}
if (node != injectionHost) {
@@ -81,7 +77,7 @@ public abstract class InjectedLanguageBlockBuilder {
for (ASTNode n = injectionHost.getTreeParent(), prev = injectionHost; n != null; prev = n, n = n.getTreeParent()) {
shift += n.getStartOffset() - prev.getStartOffset();
if (n == node) {
textRange = textRange.shiftRight(shift);
injectionRange = injectionRange.shiftRight(shift);
canProcess = true;
break;
}
@@ -92,47 +88,60 @@ public abstract class InjectedLanguageBlockBuilder {
}
String childText;
if (injectionHost.getTextLength() == textRange.getEndOffset() && textRange.getStartOffset() == 0 ||
canProcessFragment((childText = injectionHost.getText()).substring(0, textRange.getStartOffset()), injectionHost) &&
canProcessFragment(childText.substring(textRange.getEndOffset()), injectionHost)) {
injectedFile[0] = injectedPsi;
injectedRangeInsideHost.set(textRange);
prefixLength.set(shred.getPrefix().length());
suffixLength.set(shred.getSuffix().length());
if (injectionHost.getTextLength() == injectionRange.getEndOffset() && injectionRange.getStartOffset() == 0 ||
canProcessFragment((childText = injectionHost.getText()).substring(0, injectionRange.getStartOffset()), injectionHost) &&
canProcessFragment(childText.substring(injectionRange.getEndOffset()), injectionHost)) {
// inject language block
final Language childLanguage = injectedPsi.getLanguage();
final FormattingModelBuilder builder = LanguageFormatting.INSTANCE.forContext(childLanguage, injectionHost.getPsi());
if (builder != null) {
final int startOffset = injectionRange.getStartOffset();
final int endOffset = injectionRange.getEndOffset();
TextRange range = injectionHost.getTextRange();
final int prefixLength = shred.getPrefix().length();
final int suffixLength = shred.getSuffix().length();
int childOffset = range.getStartOffset();
if (lastInjectionEndOffset.get() < startOffset) {
result.add(createBlock(injectionHost, wrap, alignment, indent, new TextRange(lastInjectionEndOffset.get(), startOffset)));
}
addInjectedLanguageBlockWrapper(result, injectedPsi.getNode(), indent, childOffset + startOffset,
new TextRange(prefixLength, injectedPsi.getTextLength() - suffixLength));
lastInjectionEndOffset.set(endOffset);
}
}
};
final PsiElement injectionHostPsi = injectionHost.getPsi();
PsiFile containingFile = injectionHostPsi.getContainingFile();
InjectedLanguageManager.getInstance(containingFile.getProject()).enumerateEx(injectionHostPsi, containingFile, true, injectedPsiVisitor);
InjectedLanguageManager.getInstance(containingFile.getProject())
.enumerateEx(injectionHostPsi, containingFile, true, injectedPsiVisitor);
if (injectedFile[0] != null) {
final Language childLanguage = injectedFile[0].getLanguage();
final FormattingModelBuilder builder = LanguageFormatting.INSTANCE.forContext(childLanguage, injectionHostPsi);
if (builder != null) {
final int startOffset = injectedRangeInsideHost.get().getStartOffset();
final int endOffset = injectedRangeInsideHost.get().getEndOffset();
TextRange range = injectionHostToUse.get().getTextRange();
int childOffset = range.getStartOffset();
if (startOffset != 0) {
final ASTNode leaf = injectionHostToUse.get().findLeafElementAt(startOffset - 1);
result.add(createBlockBeforeInjection(leaf, wrap, alignment, indent, new TextRange(childOffset, childOffset + startOffset)));
}
addInjectedLanguageBlockWrapper(result, injectedFile[0].getNode(), indent, childOffset + startOffset,
new TextRange(prefixLength.get(), injectedFile[0].getTextLength() - suffixLength.get()));
if (endOffset != injectionHostToUse.get().getTextLength()) {
final ASTNode leaf = injectionHostToUse.get().findLeafElementAt(endOffset);
result.add(createBlockAfterInjection(leaf, wrap, alignment, indent, new TextRange(childOffset + endOffset, range.getEndOffset())));
}
return true;
if (lastInjectionEndOffset.get() > 0) {
if (lastInjectionEndOffset.get() < injectionHost.getTextLength()) {
result.add(createBlock(injectionHost, wrap, alignment, indent,
new TextRange(lastInjectionEndOffset.get(), injectionHost.getTextLength())));
}
return true;
}
return false;
}
private Block createBlock(ASTNode injectionHost, Wrap wrap, Alignment alignment, Indent indent, TextRange range) {
if (range.getStartOffset() == 0) {
final ASTNode leaf = injectionHost.findLeafElementAt(range.getEndOffset() - 1);
return createBlockBeforeInjection(
leaf, wrap, alignment, indent, range.shiftRight(injectionHost.getStartOffset()));
}
final ASTNode leaf = injectionHost.findLeafElementAt(range.getStartOffset());
return createBlockAfterInjection(
leaf, wrap, alignment, indent, range.shiftRight(injectionHost.getStartOffset()));
}
public void addInjectedLanguageBlockWrapper(final List<? super Block> result, final ASTNode injectedNode,
final Indent indent, int offset, @Nullable TextRange range) {
@@ -141,11 +150,11 @@ public abstract class InjectedLanguageBlockBuilder {
//
if (range != null) {
if (range.getLength() == 0) return;
if(StringUtil.isEmptyOrSpaces(range.substring(injectedNode.getText()))) {
if (StringUtil.isEmptyOrSpaces(range.substring(injectedNode.getText()))) {
return;
}
}
final PsiElement childPsi = injectedNode.getPsi();
final Language childLanguage = childPsi.getLanguage();
final FormattingModelBuilder builder = LanguageFormatting.INSTANCE.forContext(childLanguage, childPsi);
+1
View File
@@ -36,6 +36,7 @@
<orderEntry type="module" module-name="intellij.platform.builtInServer" />
<orderEntry type="library" name="NanoXML" level="project" />
<orderEntry type="library" name="Trove4j" level="project" />
<orderEntry type="library" name="fastutil-min" level="project" />
</component>
<component name="copyright">
<Base>
@@ -22,6 +22,7 @@ import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import com.intellij.psi.xml.*;
import com.intellij.util.containers.JBIterable;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -63,7 +64,7 @@ public abstract class AbstractXmlBlock extends AbstractBlock {
*
* @return True if the space must be preserved (xml:space='preserve'), false if the attribute
* contains 'default'. If the attribute is not defined, return the current value.
*/
*/
private static boolean shouldPreserveSpace(ASTNode node, boolean defaultValue) {
if (node.getPsi() instanceof XmlTag) {
XmlTag tag = (XmlTag)node.getPsi();
@@ -82,7 +83,7 @@ public abstract class AbstractXmlBlock extends AbstractBlock {
}
return defaultValue;
}
public boolean isPreserveSpace() {
return myPreserveSpace;
}
@@ -207,7 +208,7 @@ public abstract class AbstractXmlBlock extends AbstractBlock {
else if (!isBuildIndentsOnly()) {
myInjectedBlockBuilder.addInjectedLanguageBlockWrapper(result, child, indent, 0, null);
}
return child;
}
@@ -279,13 +280,25 @@ public abstract class AbstractXmlBlock extends AbstractBlock {
);
}
else {
result.add(createSimpleChild(child, indent, wrap, alignment));
result.add(createSimpleChild(child, indent, wrap, alignment, null));
}
}
/** @deprecated use and override {@code createSimpleChild } overload with {@code range } provided */
@SuppressWarnings("DeprecatedIsStillUsed")
@Deprecated
@ApiStatus.ScheduledForRemoval(inVersion = "2020.3")
protected XmlBlock createSimpleChild(final ASTNode child, final Indent indent, final Wrap wrap, final Alignment alignment) {
return new XmlBlock(child, wrap, alignment, myXmlFormattingPolicy, indent, null, isPreserveSpace());
return null;
}
protected @NotNull XmlBlock createSimpleChild(@NotNull ASTNode child, @Nullable Indent indent,
@Nullable Wrap wrap, @Nullable Alignment alignment, @Nullable TextRange range) {
XmlBlock blockFromDeprecatedCall = createSimpleChild(child, indent, wrap, alignment);
if (blockFromDeprecatedCall != null) {
return blockFromDeprecatedCall;
}
return new XmlBlock(child, wrap, alignment, myXmlFormattingPolicy, indent, range, isPreserveSpace());
}
protected XmlTagBlock createTagBlock(final ASTNode child, final Indent indent, final Wrap wrap, final Alignment alignment) {
@@ -294,7 +307,7 @@ public abstract class AbstractXmlBlock extends AbstractBlock {
@Nullable
protected XmlTag findXmlTagAt(final ASTNode child, final int startOffset) {
return null;
return null;
}
@Nullable
@@ -351,7 +364,7 @@ public abstract class AbstractXmlBlock extends AbstractBlock {
}
public abstract boolean insertLineBreakBeforeTag();
public int getBlankLinesBeforeTag() {
return insertLineBreakBeforeTag() ? 1 : 0;
}
@@ -416,7 +429,7 @@ public abstract class AbstractXmlBlock extends AbstractBlock {
protected boolean buildInjectedPsiBlocks(List<Block> result, final ASTNode child, Wrap wrap, Alignment alignment, Indent indent) {
if (isBuildIndentsOnly()) return false;
if (myInjectedBlockBuilder.addInjectedBlocks(result, child, wrap, alignment, indent)) {
return true;
}
@@ -66,14 +66,11 @@ public class XmlInjectedLanguageBlockBuilder extends InjectedLanguageBlockBuilde
public boolean canProcessFragment(String text, final ASTNode injectionHost) {
IElementType type = injectionHost.getElementType();
if (type == XmlElementType.XML_TEXT) {
text = text.trim();
text = text.replace("<![CDATA[", "");
text = text.replace("]]>", "");
return true;
}
else if (type == XmlElementType.XML_COMMENT) { // <!--[if IE]>, <![endif]--> of conditional comments injection
return true;
}
return text.isEmpty();
}
@@ -17,17 +17,23 @@ package com.intellij.psi.formatter.xml;
import com.intellij.formatting.*;
import com.intellij.lang.ASTNode;
import com.intellij.psi.PsiElement;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.xml.XmlElementType;
import com.intellij.psi.xml.XmlTag;
import com.intellij.psi.xml.XmlTokenType;
import com.intellij.util.SmartList;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import static com.intellij.util.ObjectUtils.notNull;
import static com.intellij.util.containers.ContainerUtil.addIfNotNull;
public class XmlTagBlock extends AbstractXmlBlock{
private final Indent myIndent;
@@ -162,17 +168,21 @@ public class XmlTagBlock extends AbstractXmlBlock{
@Override
@Nullable
protected
ASTNode processChild(List<Block> result, final ASTNode child, final Wrap wrap, final Alignment alignment, final Indent indent) {
protected ASTNode processChild(List<Block> result, final ASTNode child, final Wrap wrap, final Alignment alignment, final Indent indent) {
IElementType type = child.getElementType();
if (type == XmlElementType.XML_TEXT) {
final PsiElement parent = child.getPsi().getParent();
if (parent instanceof XmlTag && ((XmlTag)parent).getSubTags().length == 0) {
if (buildInjectedPsiBlocks(result, child, wrap, alignment, indent)) return child;
List<Block> injections = new SmartList<>();
if (buildInjectedPsiBlocks(injections, child, wrap, alignment, indent)) {
List<Block> regular = new SmartList<>();
createXmlTextBlocks(regular, child, wrap, alignment);
splitRegularBlocksWithInjected(child, result, injections, regular);
return child;
}
return createXmlTextBlocks(result, child, wrap, alignment);
} else if (type == XmlElementType.XML_COMMENT) {
else {
return createXmlTextBlocks(result, child, wrap, alignment);
}
}
else if (type == XmlElementType.XML_COMMENT) {
if (buildInjectedPsiBlocks(result, child, wrap, alignment, indent)) return child;
return super.processChild(result, child, wrap, alignment, indent);
}
@@ -181,6 +191,112 @@ public class XmlTagBlock extends AbstractXmlBlock{
}
}
private void splitRegularBlocksWithInjected(@NotNull ASTNode injectionHost, @NotNull List<Block> result,
@NotNull List<Block> withInjections, @NotNull List<Block> regularBlocks) {
int i = 0;
int j = 0;
int injectionHostOffset = injectionHost.getStartOffset();
// Since there are possible injections without formatter blocks (i.e. whitespace only block),
// we need to detect such scenarios and correctly split as if there was an injection.
List<TextRange> injectedRanges = new ArrayList<>(withInjections.size());
Int2ObjectMap<Block> injectedBlocksMap = new Int2ObjectOpenHashMap<>();
boolean lastInjected = true;
int lastOffset = 0;
for (Block block: withInjections) {
TextRange range = block.getTextRange();
if (block instanceof AnotherLanguageBlockWrapper) {
injectedRanges.add(range);
injectedBlocksMap.put(range.getStartOffset(), block);
lastInjected = true;
} else {
if (!lastInjected) {
// add range for empty injection
int offset = range.getStartOffset();
injectedRanges.add(new TextRange(lastOffset, offset));
}
lastOffset = range.getEndOffset();
lastInjected = false;
}
}
// Perform actual splitting
int injectedRangesCount = injectedRanges.size();
while (i < regularBlocks.size()) {
Block reg = regularBlocks.get(i);
if (j < injectedRangesCount) {
TextRange injRange = injectedRanges.get(j);
TextRange regRange = reg.getTextRange();
if (regRange.getEndOffset() <= injRange.getStartOffset()) {
// Regular block does not intersect with injected - add
result.add(reg);
i++;
}
else if (injRange.getStartOffset() <= regRange.getStartOffset()
&& regRange.getEndOffset() <= injRange.getEndOffset()) {
// Regular block completely within injected - skip
i++;
}
else {
if (regRange.getStartOffset() < injRange.getStartOffset()) {
// Regular block ends within or after an injected - split
ASTNode node = notNull(injectionHost.findLeafElementAt(injRange.getStartOffset() - 1 - injectionHostOffset), injectionHost);
result.add(createSimpleChild(node, reg.getIndent(), reg.getWrap(), reg.getAlignment(), new TextRange(
regRange.getStartOffset(), injRange.getStartOffset())));
if (regRange.getEndOffset() <= injRange.getEndOffset()) {
// Block ends within injected - move to the next block
i++;
continue;
}
// Case of a regular block spanning over multiple injected ones
}
// Add injected block to the result
addIfNotNull(result, injectedBlocksMap.get(injRange.getStartOffset()));
j++;
if (regRange.getStartOffset() < injRange.getEndOffset()) {
// we have a regular block starting within or before last added injected block - split
int lastInjection = injRange.getEndOffset();
while (j < injectedRangesCount) {
// check if single block does not span over next injected block
TextRange nextRange = injectedRanges.get(j);
if (nextRange.getStartOffset() < regRange.getEndOffset()) {
// out regular block ends within or after the next injected block - add a split if it's not empty
if (lastInjection < nextRange.getStartOffset()) {
ASTNode node = notNull(injectionHost.findLeafElementAt(lastInjection - injectionHostOffset), injectionHost);
result.add(createSimpleChild(node, reg.getIndent(), reg.getWrap(), reg.getAlignment(),
new TextRange(lastInjection, nextRange.getStartOffset())));
}
lastInjection = nextRange.getEndOffset();
if (lastInjection <= regRange.getEndOffset()) {
// Add current injected block and repeat if regular block ends after it
addIfNotNull(result, injectedBlocksMap.get(nextRange.getStartOffset()));
j++;
continue;
}
}
break;
}
// We might have some leftover of regular block after last added injected one,
// which does not end within or after the next injected block - add a split
if (lastInjection < regRange.getEndOffset()) {
ASTNode node = notNull(injectionHost.findLeafElementAt(lastInjection - injectionHostOffset), injectionHost);
result.add(createSimpleChild(node, reg.getIndent(), reg.getWrap(), reg.getAlignment(),
new TextRange(lastInjection, regRange.getEndOffset())));
}
i++;
}
}
}
else {
// No more injected blocks to process, just add the regular ones
result.add(reg);
i++;
}
}
// Add any leftover injected blocks
while (j < injectedRangesCount) {
addIfNotNull(result, injectedBlocksMap.get(injectedRanges.get(j++).getStartOffset()));
}
}
protected Indent getChildrenIndent() {
return myXmlFormattingPolicy.indentChildrenOf(getTag())
? Indent.getNormalIndent()
@@ -23,10 +23,13 @@ import com.intellij.psi.formatter.xml.*;
import com.intellij.psi.templateLanguages.OuterLanguageElement;
import com.intellij.psi.xml.XmlElementType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import static com.intellij.util.ObjectUtils.notNull;
public class TemplateXmlBlock extends XmlBlock implements IndentInheritingBlock {
private final AbstractXmlTemplateFormattingModelBuilder myBuilder;
private Indent myIndent;
@@ -45,8 +48,9 @@ public class TemplateXmlBlock extends XmlBlock implements IndentInheritingBlock
}
@Override
protected XmlBlock createSimpleChild(ASTNode child, Indent indent, Wrap wrap, Alignment alignment) {
return myBuilder.createXmlBlock(child, wrap, alignment, myXmlFormattingPolicy,indent, child.getTextRange());
protected @NotNull XmlBlock createSimpleChild(@NotNull ASTNode child, @Nullable Indent indent,
@Nullable Wrap wrap, @Nullable Alignment alignment, @Nullable TextRange range) {
return myBuilder.createXmlBlock(child, wrap, alignment, myXmlFormattingPolicy, indent, notNull(range, child.getTextRange()));
}
@Override
@@ -20,10 +20,13 @@ import com.intellij.formatting.Block;
import com.intellij.formatting.Indent;
import com.intellij.formatting.Wrap;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.formatter.xml.XmlBlock;
import com.intellij.psi.formatter.xml.XmlFormattingPolicy;
import com.intellij.psi.formatter.xml.XmlTagBlock;
import com.intellij.xml.util.HtmlUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
@@ -60,8 +63,9 @@ public class TemplateXmlTagBlock extends XmlTagBlock implements IndentInheriting
@Override
protected XmlBlock createSimpleChild(ASTNode child, Indent indent, Wrap wrap, Alignment alignment) {
return myBuilder.createXmlBlock(child, wrap, alignment, myXmlFormattingPolicy, indent, null);
protected @NotNull XmlBlock createSimpleChild(@NotNull ASTNode child, @Nullable Indent indent,
@Nullable Wrap wrap, @Nullable Alignment alignment, @Nullable TextRange range) {
return myBuilder.createXmlBlock(child, wrap, alignment, myXmlFormattingPolicy, indent, range);
}
@Override